@alpaca-software/40kdc-data 0.4.6 → 0.4.8

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.
@@ -49,6 +49,31 @@ export type Footprint = {
49
49
  points: Vec2[];
50
50
  };
51
51
  export type Mirror = "none" | "horizontal" | "vertical";
52
+ /** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */
53
+ export type BoardEdge = "left" | "right" | "top" | "bottom";
54
+ /**
55
+ * Which feature of the placed area a dimension line reaches: a specific
56
+ * footprint vertex (by index, in {@link footprintVertices} order), or one of
57
+ * the placed area's axis-aligned bounding faces ("the left face", etc.).
58
+ */
59
+ export type FeatureRef = {
60
+ kind: "vertex";
61
+ index: number;
62
+ } | {
63
+ kind: "face";
64
+ side: "min-x" | "max-x" | "min-y" | "max-y";
65
+ };
66
+ /**
67
+ * One authored measurement keystone: the dimension line a reference card
68
+ * prints so a player can place the piece with a tape measure. Only the
69
+ * selection is stored — the distance is always derived from resolved geometry
70
+ * (see `keystoneMeasurements`), so a keystone can never disagree with the
71
+ * layout.
72
+ */
73
+ export interface Keystone {
74
+ edge: BoardEdge;
75
+ ref: FeatureRef;
76
+ }
52
77
  export interface ComposedFeature {
53
78
  id?: string;
54
79
  template: string;
@@ -81,6 +106,7 @@ export interface LayoutPiece {
81
106
  height_inches?: number;
82
107
  terrain_area_keywords?: string[];
83
108
  link_group?: string;
109
+ keystones?: Keystone[];
84
110
  }
85
111
  export interface TerrainLayout {
86
112
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/terrain/resolve.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/terrain/resolve.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/terrain/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAkEH,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC;AAE1B,4EAA4E;AAC5E,MAAM,UAAU,iBAAiB,CAAC,EAAa;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,WAAW;YACd,OAAO;gBACL,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;gBACd,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;gBACrB,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE;gBAC7B,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE;aACvB,CAAC;QACJ,KAAK,gBAAgB;YACnB,yDAAyD;YACzD,OAAO;gBACL,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;gBACd,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;gBACrB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE;aACvB,CAAC;QACJ,KAAK,SAAS;YACZ,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,UAAU,GAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACpC,SAAS,IAAI,KAAK,CAAC;QACnB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;QAC1B,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;IAC5B,CAAC;IACD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5F,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,SAAS,WAAW,CAAC,CAAO,EAAE,CAAS;IACrC,QAAQ,CAAC,EAAE,CAAC;QACV,KAAK,YAAY;YACf,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,KAAK,UAAU;YACb,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,SAAS,QAAQ,CAAC,CAAO,EAAE,GAAW;IACpC,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,MAAM,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC;IACpB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtB,OAAO,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,kFAAkF;AAClF,SAAS,MAAM,CAAC,CAAO,EAAE,QAAgB,EAAE,MAAc;IACvD,OAAO,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,SAAoB,EAAE,QAAgB,EAAE,MAAc;IACpF,MAAM,KAAK,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CACrB,EAAa,EACb,QAAc,EACd,QAAgB,EAChB,MAAc;IAEd,MAAM,KAAK,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC;IACpC,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACrB,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,SAAS,MAAM,CAAC,CAAO;IACrB,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,GAAG,YAAY,EAAE,CAAC;AAChH,CAAC;AAED,SAAS,cAAc,CAAC,KAAqC;IAC3D,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAqB,EAAE,SAA4B;IAC/E,MAAM,IAAI,GAAG,IAAI,GAAG,EAA2B,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,SAAS;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAuB,CAAC;IACjD,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,CAAC,EAAE;YAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAEzD,MAAM,WAAW,GAAG,CAAC,KAAmD,EAAE,KAAa,EAAa,EAAE;QACpG,IAAI,KAAK,CAAC,SAAS;YAAE,OAAO,KAAK,CAAC,SAAS,CAAC;QAC5C,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnC,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,uBAAuB,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC;YACxF,OAAO,CAAC,CAAC,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,4CAA4C,CAAC,CAAC;IACtF,CAAC,CAAC;IAEF,MAAM,GAAG,GAAoB,EAAE,CAAC;IAEhC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC;QAClD,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,gBAAgB,IAAI,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC;QACtC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAElF,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;YACzB,4DAA4D;YAC5D,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YACnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,6BAA6B,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;YAC9F,CAAC;YACD,MAAM,SAAS,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACvE,MAAM,IAAI,GAAG,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAC;YAC1C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC;YACxC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;gBACnC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBACnC,OAAO,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;YAC5E,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjG,SAAS;QACX,CAAC;QAED,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEjG,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnC,KAAK,MAAM,IAAI,IAAI,CAAC,EAAE,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACrC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACnC,IAAI,CAAC,EAAE,EAAE,CAAC;oBACR,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,mDAAmD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAC7G,CAAC;gBACD,MAAM,SAAS,GAAG,cAAc,CAC9B,EAAE,CAAC,SAAS,EACZ,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,gBAAgB,IAAI,CAAC,EAC1B,IAAI,CAAC,MAAM,IAAI,MAAM,CACtB,CAAC;gBACF,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;oBACpC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;oBACtC,OAAO,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;gBAC1E,CAAC,CAAC,CAAC;gBACH,GAAG,CAAC,IAAI,CAAC;oBACP,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI;oBACnB,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI;oBACrB,UAAU,EAAE,SAAS;oBACrB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;oBACtB,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Terrain layout resolver — turns a {@link TerrainLayout} (template references +\n * centroid-anchored placements + rotation/mirror) into absolute board-space\n * polygon vertices. This is the shared geometry contract pinned by the\n * `conformance/terrain-resolver` corpus; the Rust crate implements the same\n * function and must reproduce these vertices byte-for-byte (4-dp rounded).\n *\n * ## Transform contract\n *\n * Frames are board inches, origin at a board corner, **y-down** (per\n * `common.schema.json#/$defs/vec2`). A footprint is authored in natural local\n * y-down coordinates; the resolver derives its **polygon area centroid** and\n * treats local vertices as `(v - centroid)`, so `position` always denotes the\n * centroid and is invariant under rotation and mirror.\n *\n * Local → board, for an unparented piece, is `mirror → rotate → translate`:\n *\n * board = position + R_cw(rotation) · M(mirror) · (v - centroid)\n *\n * with `M`: horizontal → (-x, y), vertical → (x, -y); and `R_cw(θ)` a clockwise\n * rotation in the y-down frame, `[[cosθ, -sinθ], [sinθ, cosθ]]`.\n *\n * A feature with a `parent_area_id` (or a template's composed feature) is first\n * placed in the parent area's **centroid-local frame** (origin at the area\n * centroid), then carried through the area's own placement:\n *\n * board = T_area ∘ R_area ∘ M_area ( featurePos + R_feat · M_feat · (w - C_feat) )\n *\n * ## Emission order (a pinned invariant)\n *\n * Pieces are emitted in `layout.pieces` order. When a piece instances an area\n * template that carries composed `features`, those features are emitted\n * immediately after their area, in template-declaration order.\n */\n\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\nexport type Footprint =\n | { type: \"rectangle\"; width: number; height: number }\n | { type: \"right-triangle\"; width: number; height: number }\n | { type: \"polygon\"; points: Vec2[] };\n\nexport type Mirror = \"none\" | \"horizontal\" | \"vertical\";\n\nexport interface ComposedFeature {\n id?: string;\n template: string;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n floor?: number;\n}\n\nexport interface TerrainTemplate {\n id: string;\n name?: string;\n kind: \"area\" | \"feature\";\n footprint: Footprint;\n default_height_inches?: number;\n default_blocking?: boolean;\n default_terrain_area_keywords?: string[];\n features?: ComposedFeature[];\n}\n\nexport interface LayoutPiece {\n id?: string;\n name?: string;\n piece_type?: \"area\" | \"feature\";\n template?: string;\n footprint?: Footprint;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n parent_area_id?: string;\n floor?: number;\n height_inches?: number;\n terrain_area_keywords?: string[];\n link_group?: string;\n}\n\nexport interface TerrainLayout {\n id: string;\n name: string;\n pieces?: LayoutPiece[];\n}\n\nexport interface ResolvedPiece {\n /** Layout-local id when present, else the piece name, else null. */\n id: string | null;\n name: string | null;\n piece_type: \"area\" | \"feature\";\n floor: number;\n /** Absolute board-space polygon vertices, y-down. */\n vertices: Vec2[];\n}\n\nconst DEG = Math.PI / 180;\n\n/** A footprint's polygon vertices in natural local (y-down) coordinates. */\nexport function footprintVertices(fp: Footprint): Vec2[] {\n switch (fp.type) {\n case \"rectangle\":\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: fp.width, y: fp.height },\n { x: 0, y: fp.height },\n ];\n case \"right-triangle\":\n // Right angle at the local origin, legs along +x and +y.\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: 0, y: fp.height },\n ];\n case \"polygon\":\n return fp.points.map((p) => ({ x: p.x, y: p.y }));\n default: {\n const exhaustive: never = fp;\n throw new Error(`unknown footprint type: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/**\n * Polygon area centroid (shoelace). Falls back to the vertex mean when the\n * polygon is degenerate (zero signed area, e.g. collinear points) so the\n * resolver never divides by zero.\n */\nexport function polygonCentroid(verts: Vec2[]): Vec2 {\n const n = verts.length;\n if (n === 0) return { x: 0, y: 0 };\n let twiceArea = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < n; i++) {\n const a = verts[i];\n const b = verts[(i + 1) % n];\n const cross = a.x * b.y - b.x * a.y;\n twiceArea += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n if (twiceArea === 0) {\n const mean = verts.reduce((acc, v) => ({ x: acc.x + v.x, y: acc.y + v.y }), { x: 0, y: 0 });\n return { x: mean.x / n, y: mean.y / n };\n }\n return { x: cx / (3 * twiceArea), y: cy / (3 * twiceArea) };\n}\n\nfunction applyMirror(v: Vec2, m: Mirror): Vec2 {\n switch (m) {\n case \"horizontal\":\n return { x: -v.x, y: v.y };\n case \"vertical\":\n return { x: v.x, y: -v.y };\n default:\n return v;\n }\n}\n\n/** Clockwise rotation by `deg` degrees in the y-down frame. */\nfunction rotateCw(v: Vec2, deg: number): Vec2 {\n if (deg === 0) return { x: v.x, y: v.y };\n const r = deg * DEG;\n const c = Math.cos(r);\n const s = Math.sin(r);\n return { x: c * v.x - s * v.y, y: s * v.x + c * v.y };\n}\n\n/** mirror → rotate (no translation). The orientation-only part of a placement. */\nfunction orient(v: Vec2, rotation: number, mirror: Mirror): Vec2 {\n return rotateCw(applyMirror(v, mirror), rotation);\n}\n\n/**\n * The board-space offset of each footprint vertex from the piece centroid,\n * after mirror + rotation but before translation. Adding `position` to each\n * gives the resolved board vertices; this is the orientation-only part a\n * card-measurement solver inverts to recover the centroid. Vertex order matches\n * {@link footprintVertices}.\n */\nexport function orientedOffsets(footprint: Footprint, rotation: number, mirror: Mirror): Vec2[] {\n const verts = footprintVertices(footprint);\n const c = polygonCentroid(verts);\n return verts.map((v) => orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror));\n}\n\n/**\n * Place a footprint's local vertices into a target frame: recenter on the\n * footprint centroid, mirror, rotate, then translate so the centroid lands on\n * `position`. The target frame is board space for an area, or the parent area's\n * centroid-local frame for a composed/parented feature.\n */\nfunction placeFootprint(\n fp: Footprint,\n position: Vec2,\n rotation: number,\n mirror: Mirror,\n): Vec2[] {\n const verts = footprintVertices(fp);\n const c = polygonCentroid(verts);\n return verts.map((v) => {\n const o = orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror);\n return { x: o.x + position.x, y: o.y + position.y };\n });\n}\n\nconst TWO_DP_ROUND = 1e4;\nfunction round4(v: Vec2): Vec2 {\n return { x: Math.round(v.x * TWO_DP_ROUND) / TWO_DP_ROUND, y: Math.round(v.y * TWO_DP_ROUND) / TWO_DP_ROUND };\n}\n\nfunction resolvedIdName(piece: { id?: string; name?: string }): { id: string | null; name: string | null } {\n return { id: piece.id ?? null, name: piece.name ?? null };\n}\n\nexport class TerrainResolveError extends Error {}\n\n/**\n * Resolve a layout to absolute board-space vertices per piece. `templates` is\n * the catalog a piece's `template` references resolve against.\n */\nexport function resolveLayout(layout: TerrainLayout, templates: TerrainTemplate[]): ResolvedPiece[] {\n const byId = new Map<string, TerrainTemplate>();\n for (const t of templates) byId.set(t.id, t);\n\n const pieces = layout.pieces ?? [];\n const areasById = new Map<string, LayoutPiece>();\n for (const p of pieces) if (p.id) areasById.set(p.id, p);\n\n const footprintOf = (piece: { template?: string; footprint?: Footprint }, where: string): Footprint => {\n if (piece.footprint) return piece.footprint;\n if (piece.template) {\n const t = byId.get(piece.template);\n if (!t) throw new TerrainResolveError(`${where}: unknown template \"${piece.template}\"`);\n return t.footprint;\n }\n throw new TerrainResolveError(`${where}: piece has neither footprint nor template`);\n };\n\n const out: ResolvedPiece[] = [];\n\n for (const piece of pieces) {\n const where = piece.id ?? piece.name ?? \"<piece>\";\n const fp = footprintOf(piece, where);\n const rotation = piece.rotation_degrees ?? 0;\n const mirror = piece.mirror ?? \"none\";\n const pieceType = piece.piece_type ?? (piece.parent_area_id ? \"feature\" : \"area\");\n\n if (piece.parent_area_id) {\n // Feature placed in its parent area's centroid-local frame.\n const parent = areasById.get(piece.parent_area_id);\n if (!parent) {\n throw new TerrainResolveError(`${where}: unknown parent_area_id \"${piece.parent_area_id}\"`);\n }\n const areaLocal = placeFootprint(fp, piece.position, rotation, mirror);\n const aRot = parent.rotation_degrees ?? 0;\n const aMirror = parent.mirror ?? \"none\";\n const vertices = areaLocal.map((p) => {\n const o = orient(p, aRot, aMirror);\n return round4({ x: o.x + parent.position.x, y: o.y + parent.position.y });\n });\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n continue;\n }\n\n // Unparented area or feature: place directly in board space.\n const vertices = placeFootprint(fp, piece.position, rotation, mirror).map(round4);\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n\n // Expand an area template's composed features, carried through this area's\n // placement (same composition math as a parented feature).\n if (piece.template) {\n const t = byId.get(piece.template);\n for (const feat of t?.features ?? []) {\n const ft = byId.get(feat.template);\n if (!ft) {\n throw new TerrainResolveError(`${where}: composed feature references unknown template \"${feat.template}\"`);\n }\n const areaLocal = placeFootprint(\n ft.footprint,\n feat.position,\n feat.rotation_degrees ?? 0,\n feat.mirror ?? \"none\",\n );\n const featVerts = areaLocal.map((p) => {\n const o = orient(p, rotation, mirror);\n return round4({ x: o.x + piece.position.x, y: o.y + piece.position.y });\n });\n out.push({\n id: feat.id ?? null,\n name: ft.name ?? null,\n piece_type: \"feature\",\n floor: feat.floor ?? 0,\n vertices: featVerts,\n });\n }\n }\n }\n\n return out;\n}\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/terrain/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Terrain layout resolver — turns a {@link TerrainLayout} (template references +\n * centroid-anchored placements + rotation/mirror) into absolute board-space\n * polygon vertices. This is the shared geometry contract pinned by the\n * `conformance/terrain-resolver` corpus; the Rust crate implements the same\n * function and must reproduce these vertices byte-for-byte (4-dp rounded).\n *\n * ## Transform contract\n *\n * Frames are board inches, origin at a board corner, **y-down** (per\n * `common.schema.json#/$defs/vec2`). A footprint is authored in natural local\n * y-down coordinates; the resolver derives its **polygon area centroid** and\n * treats local vertices as `(v - centroid)`, so `position` always denotes the\n * centroid and is invariant under rotation and mirror.\n *\n * Local → board, for an unparented piece, is `mirror → rotate → translate`:\n *\n * board = position + R_cw(rotation) · M(mirror) · (v - centroid)\n *\n * with `M`: horizontal → (-x, y), vertical → (x, -y); and `R_cw(θ)` a clockwise\n * rotation in the y-down frame, `[[cosθ, -sinθ], [sinθ, cosθ]]`.\n *\n * A feature with a `parent_area_id` (or a template's composed feature) is first\n * placed in the parent area's **centroid-local frame** (origin at the area\n * centroid), then carried through the area's own placement:\n *\n * board = T_area ∘ R_area ∘ M_area ( featurePos + R_feat · M_feat · (w - C_feat) )\n *\n * ## Emission order (a pinned invariant)\n *\n * Pieces are emitted in `layout.pieces` order. When a piece instances an area\n * template that carries composed `features`, those features are emitted\n * immediately after their area, in template-declaration order.\n */\n\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\nexport type Footprint =\n | { type: \"rectangle\"; width: number; height: number }\n | { type: \"right-triangle\"; width: number; height: number }\n | { type: \"polygon\"; points: Vec2[] };\n\nexport type Mirror = \"none\" | \"horizontal\" | \"vertical\";\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/**\n * One authored measurement keystone: the dimension line a reference card\n * prints so a player can place the piece with a tape measure. Only the\n * selection is stored — the distance is always derived from resolved geometry\n * (see `keystoneMeasurements`), so a keystone can never disagree with the\n * layout.\n */\nexport interface Keystone {\n edge: BoardEdge;\n ref: FeatureRef;\n}\n\nexport interface ComposedFeature {\n id?: string;\n template: string;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n floor?: number;\n}\n\nexport interface TerrainTemplate {\n id: string;\n name?: string;\n kind: \"area\" | \"feature\";\n footprint: Footprint;\n default_height_inches?: number;\n default_blocking?: boolean;\n default_terrain_area_keywords?: string[];\n features?: ComposedFeature[];\n}\n\nexport interface LayoutPiece {\n id?: string;\n name?: string;\n piece_type?: \"area\" | \"feature\";\n template?: string;\n footprint?: Footprint;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n parent_area_id?: string;\n floor?: number;\n height_inches?: number;\n terrain_area_keywords?: string[];\n link_group?: string;\n keystones?: Keystone[];\n}\n\nexport interface TerrainLayout {\n id: string;\n name: string;\n pieces?: LayoutPiece[];\n}\n\nexport interface ResolvedPiece {\n /** Layout-local id when present, else the piece name, else null. */\n id: string | null;\n name: string | null;\n piece_type: \"area\" | \"feature\";\n floor: number;\n /** Absolute board-space polygon vertices, y-down. */\n vertices: Vec2[];\n}\n\nconst DEG = Math.PI / 180;\n\n/** A footprint's polygon vertices in natural local (y-down) coordinates. */\nexport function footprintVertices(fp: Footprint): Vec2[] {\n switch (fp.type) {\n case \"rectangle\":\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: fp.width, y: fp.height },\n { x: 0, y: fp.height },\n ];\n case \"right-triangle\":\n // Right angle at the local origin, legs along +x and +y.\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: 0, y: fp.height },\n ];\n case \"polygon\":\n return fp.points.map((p) => ({ x: p.x, y: p.y }));\n default: {\n const exhaustive: never = fp;\n throw new Error(`unknown footprint type: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/**\n * Polygon area centroid (shoelace). Falls back to the vertex mean when the\n * polygon is degenerate (zero signed area, e.g. collinear points) so the\n * resolver never divides by zero.\n */\nexport function polygonCentroid(verts: Vec2[]): Vec2 {\n const n = verts.length;\n if (n === 0) return { x: 0, y: 0 };\n let twiceArea = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < n; i++) {\n const a = verts[i];\n const b = verts[(i + 1) % n];\n const cross = a.x * b.y - b.x * a.y;\n twiceArea += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n if (twiceArea === 0) {\n const mean = verts.reduce((acc, v) => ({ x: acc.x + v.x, y: acc.y + v.y }), { x: 0, y: 0 });\n return { x: mean.x / n, y: mean.y / n };\n }\n return { x: cx / (3 * twiceArea), y: cy / (3 * twiceArea) };\n}\n\nfunction applyMirror(v: Vec2, m: Mirror): Vec2 {\n switch (m) {\n case \"horizontal\":\n return { x: -v.x, y: v.y };\n case \"vertical\":\n return { x: v.x, y: -v.y };\n default:\n return v;\n }\n}\n\n/** Clockwise rotation by `deg` degrees in the y-down frame. */\nfunction rotateCw(v: Vec2, deg: number): Vec2 {\n if (deg === 0) return { x: v.x, y: v.y };\n const r = deg * DEG;\n const c = Math.cos(r);\n const s = Math.sin(r);\n return { x: c * v.x - s * v.y, y: s * v.x + c * v.y };\n}\n\n/** mirror → rotate (no translation). The orientation-only part of a placement. */\nfunction orient(v: Vec2, rotation: number, mirror: Mirror): Vec2 {\n return rotateCw(applyMirror(v, mirror), rotation);\n}\n\n/**\n * The board-space offset of each footprint vertex from the piece centroid,\n * after mirror + rotation but before translation. Adding `position` to each\n * gives the resolved board vertices; this is the orientation-only part a\n * card-measurement solver inverts to recover the centroid. Vertex order matches\n * {@link footprintVertices}.\n */\nexport function orientedOffsets(footprint: Footprint, rotation: number, mirror: Mirror): Vec2[] {\n const verts = footprintVertices(footprint);\n const c = polygonCentroid(verts);\n return verts.map((v) => orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror));\n}\n\n/**\n * Place a footprint's local vertices into a target frame: recenter on the\n * footprint centroid, mirror, rotate, then translate so the centroid lands on\n * `position`. The target frame is board space for an area, or the parent area's\n * centroid-local frame for a composed/parented feature.\n */\nfunction placeFootprint(\n fp: Footprint,\n position: Vec2,\n rotation: number,\n mirror: Mirror,\n): Vec2[] {\n const verts = footprintVertices(fp);\n const c = polygonCentroid(verts);\n return verts.map((v) => {\n const o = orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror);\n return { x: o.x + position.x, y: o.y + position.y };\n });\n}\n\nconst TWO_DP_ROUND = 1e4;\nfunction round4(v: Vec2): Vec2 {\n return { x: Math.round(v.x * TWO_DP_ROUND) / TWO_DP_ROUND, y: Math.round(v.y * TWO_DP_ROUND) / TWO_DP_ROUND };\n}\n\nfunction resolvedIdName(piece: { id?: string; name?: string }): { id: string | null; name: string | null } {\n return { id: piece.id ?? null, name: piece.name ?? null };\n}\n\nexport class TerrainResolveError extends Error {}\n\n/**\n * Resolve a layout to absolute board-space vertices per piece. `templates` is\n * the catalog a piece's `template` references resolve against.\n */\nexport function resolveLayout(layout: TerrainLayout, templates: TerrainTemplate[]): ResolvedPiece[] {\n const byId = new Map<string, TerrainTemplate>();\n for (const t of templates) byId.set(t.id, t);\n\n const pieces = layout.pieces ?? [];\n const areasById = new Map<string, LayoutPiece>();\n for (const p of pieces) if (p.id) areasById.set(p.id, p);\n\n const footprintOf = (piece: { template?: string; footprint?: Footprint }, where: string): Footprint => {\n if (piece.footprint) return piece.footprint;\n if (piece.template) {\n const t = byId.get(piece.template);\n if (!t) throw new TerrainResolveError(`${where}: unknown template \"${piece.template}\"`);\n return t.footprint;\n }\n throw new TerrainResolveError(`${where}: piece has neither footprint nor template`);\n };\n\n const out: ResolvedPiece[] = [];\n\n for (const piece of pieces) {\n const where = piece.id ?? piece.name ?? \"<piece>\";\n const fp = footprintOf(piece, where);\n const rotation = piece.rotation_degrees ?? 0;\n const mirror = piece.mirror ?? \"none\";\n const pieceType = piece.piece_type ?? (piece.parent_area_id ? \"feature\" : \"area\");\n\n if (piece.parent_area_id) {\n // Feature placed in its parent area's centroid-local frame.\n const parent = areasById.get(piece.parent_area_id);\n if (!parent) {\n throw new TerrainResolveError(`${where}: unknown parent_area_id \"${piece.parent_area_id}\"`);\n }\n const areaLocal = placeFootprint(fp, piece.position, rotation, mirror);\n const aRot = parent.rotation_degrees ?? 0;\n const aMirror = parent.mirror ?? \"none\";\n const vertices = areaLocal.map((p) => {\n const o = orient(p, aRot, aMirror);\n return round4({ x: o.x + parent.position.x, y: o.y + parent.position.y });\n });\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n continue;\n }\n\n // Unparented area or feature: place directly in board space.\n const vertices = placeFootprint(fp, piece.position, rotation, mirror).map(round4);\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n\n // Expand an area template's composed features, carried through this area's\n // placement (same composition math as a parented feature).\n if (piece.template) {\n const t = byId.get(piece.template);\n for (const feat of t?.features ?? []) {\n const ft = byId.get(feat.template);\n if (!ft) {\n throw new TerrainResolveError(`${where}: composed feature references unknown template \"${feat.template}\"`);\n }\n const areaLocal = placeFootprint(\n ft.footprint,\n feat.position,\n feat.rotation_degrees ?? 0,\n feat.mirror ?? \"none\",\n );\n const featVerts = areaLocal.map((p) => {\n const o = orient(p, rotation, mirror);\n return round4({ x: o.x + piece.position.x, y: o.y + piece.position.y });\n });\n out.push({\n id: feat.id ?? null,\n name: ft.name ?? null,\n piece_type: \"feature\",\n floor: feat.floor ?? 0,\n vertices: featVerts,\n });\n }\n }\n }\n\n return out;\n}\n"]}
@@ -12,21 +12,8 @@
12
12
  * Because the centroid is rotation- and mirror-invariant, orientation is fixed
13
13
  * first; each dimension line then pins one axis of the centroid in closed form.
14
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
- };
15
+ import { type BoardEdge, type FeatureRef, type Footprint, type Mirror, type Vec2 } from "./resolve.js";
16
+ export type { BoardEdge, FeatureRef } from "./resolve.js";
30
17
  /** One card dimension line: `distance` inches from `edge` to `feature`. */
31
18
  export interface DimensionLine {
32
19
  edge: BoardEdge;
@@ -1 +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;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC;;;OAGG;IACH,KAAK,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;IACjE,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,GACtB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAuF5C"}
1
+ {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,IAAI,EACV,MAAM,cAAc,CAAC;AAItB,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1D,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;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC;;;OAGG;IACH,KAAK,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;IACjE,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,GACtB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAuF5C"}
@@ -12,7 +12,7 @@
12
12
  * Because the centroid is rotation- and mirror-invariant, orientation is fixed
13
13
  * first; each dimension line then pins one axis of the centroid in closed form.
14
14
  */
15
- import { orientedOffsets } from "./resolve.js";
15
+ import { orientedOffsets, } from "./resolve.js";
16
16
  export class TerrainSolveError extends Error {
17
17
  }
18
18
  /** The signed offset (from the centroid) the given feature resolves to, on its axis. */
@@ -1 +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\n/**\n * One triangulation measurement: `distance` inches from board `edge` to a\n * specific footprint vertex (corner). Faces are intentionally excluded — an\n * arbitrarily-rotated piece has no axis-aligned face to measure to.\n */\nexport interface TriangulationLine {\n edge: BoardEdge;\n distance: number;\n vertex: number;\n}\n\nexport interface TriangulateInput {\n footprint: Footprint;\n mirror: Mirror;\n board: { width: number; height: number };\n /**\n * Three corner measurements. At least two must share an axis (left/right or\n * top/bottom) to fix the angle, and at least one must pin the other axis.\n */\n lines: [TriangulationLine, TriangulationLine, TriangulationLine];\n /** Current rotation in degrees, used to choose between the two angle roots. */\n rotationHint?: number;\n}\n\nconst TWO_PI = Math.PI * 2;\n/** Smallest absolute angular separation between two radian angles. */\nfunction angularGap(a: number, b: number): number {\n const d = (((a - b) % TWO_PI) + TWO_PI) % TWO_PI;\n return Math.min(d, TWO_PI - d);\n}\n\n/**\n * Back-solve a piece's centroid AND rotation from three card measurements to\n * specific footprint corners — the inverse needed for pieces at non-90° angles,\n * where the card pins three corner-to-edge distances rather than one per axis.\n *\n * Closed form: with the (unknown) rotation θ, each corner `v` resolves to\n * `centroid + R(θ)·v`. Subtracting two same-axis measurements cancels the\n * centroid and leaves `A·cosθ + B·sinθ = C`, solved as `θ = atan2(B,A) ±\n * acos(C/√(A²+B²))`; the root nearest `rotationHint` is chosen. One measurement\n * on each axis then pins the centroid.\n */\nexport function solveCentroidTriangulated(\n input: TriangulateInput,\n): { x: number; y: number; rotation: number } {\n // Mirror-applied, pre-rotation offsets (θ is the unknown we're solving for).\n const offsets = orientedOffsets(input.footprint, 0, input.mirror);\n const items = input.lines.map((l) => {\n const o = offsets[l.vertex];\n if (!o) throw new TerrainSolveError(`vertex index ${l.vertex} out of range`);\n const axis = axisOfEdge(l.edge);\n let target: number;\n switch (l.edge) {\n case \"left\":\n target = l.distance;\n break;\n case \"right\":\n target = input.board.width - l.distance;\n break;\n case \"top\":\n target = l.distance;\n break;\n case \"bottom\":\n target = input.board.height - l.distance;\n break;\n }\n return { axis, target, o };\n });\n const xs = items.filter((i) => i.axis === \"x\");\n const ys = items.filter((i) => i.axis === \"y\");\n\n let pivot: typeof items;\n let pivotAxis: \"x\" | \"y\";\n if (xs.length >= 2 && ys.length >= 1) {\n pivot = xs;\n pivotAxis = \"x\";\n } else if (ys.length >= 2 && xs.length >= 1) {\n pivot = ys;\n pivotAxis = \"y\";\n } else {\n throw new TerrainSolveError(\n \"triangulation needs two measurements from one pair of edges (left/right or top/bottom) and one from the other\",\n );\n }\n\n // Best-conditioned pair on the pivot axis (corners that are furthest apart).\n let a = pivot[0];\n let b = pivot[1];\n let spread = -1;\n for (let i = 0; i < pivot.length; i++) {\n for (let j = i + 1; j < pivot.length; j++) {\n const d = Math.hypot(pivot[i].o.x - pivot[j].o.x, pivot[i].o.y - pivot[j].o.y);\n if (d > spread) {\n spread = d;\n a = pivot[i];\n b = pivot[j];\n }\n }\n }\n\n // Subtract the two same-axis equations → A·cosθ + B·sinθ = C.\n // x-axis vertex eq: cx + (cosθ·o.x − sinθ·o.y) = target\n // y-axis vertex eq: cy + (sinθ·o.x + cosθ·o.y) = target\n const dx = a.o.x - b.o.x;\n const dy = a.o.y - b.o.y;\n const C = a.target - b.target;\n const A = pivotAxis === \"x\" ? dx : dy;\n const B = pivotAxis === \"x\" ? -dy : dx;\n const R = Math.hypot(A, B);\n if (R < 1e-9) {\n throw new TerrainSolveError(\"the two same-edge measurements must reference different corners\");\n }\n const ratio = C / R;\n if (ratio > 1 + 1e-6 || ratio < -1 - 1e-6) {\n throw new TerrainSolveError(\"measurements are inconsistent — no orientation fits\");\n }\n const phi = Math.atan2(B, A);\n const base = Math.acos(Math.max(-1, Math.min(1, ratio)));\n const hint = ((input.rotationHint ?? 0) * Math.PI) / 180;\n const theta = [phi + base, phi - base].reduce((best, c) =>\n angularGap(c, hint) < angularGap(best, hint) ? c : best,\n );\n\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n const xLine = xs[0];\n const yLine = ys[0];\n const x = xLine.target - (cos * xLine.o.x - sin * xLine.o.y);\n const y = yLine.target - (sin * yLine.o.x + cos * yLine.o.y);\n const rotation = (((theta * 180) / Math.PI) % 360 + 360) % 360;\n return { x, y, rotation };\n}\n"]}
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 {\n orientedOffsets,\n type BoardEdge,\n type FeatureRef,\n type Footprint,\n type Mirror,\n type Vec2,\n} from \"./resolve.js\";\n\n// The edge/feature vocabulary lives in resolve.ts (shared with keystones);\n// re-exported here so existing solver imports keep working.\nexport type { BoardEdge, FeatureRef } from \"./resolve.js\";\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\n/**\n * One triangulation measurement: `distance` inches from board `edge` to a\n * specific footprint vertex (corner). Faces are intentionally excluded — an\n * arbitrarily-rotated piece has no axis-aligned face to measure to.\n */\nexport interface TriangulationLine {\n edge: BoardEdge;\n distance: number;\n vertex: number;\n}\n\nexport interface TriangulateInput {\n footprint: Footprint;\n mirror: Mirror;\n board: { width: number; height: number };\n /**\n * Three corner measurements. At least two must share an axis (left/right or\n * top/bottom) to fix the angle, and at least one must pin the other axis.\n */\n lines: [TriangulationLine, TriangulationLine, TriangulationLine];\n /** Current rotation in degrees, used to choose between the two angle roots. */\n rotationHint?: number;\n}\n\nconst TWO_PI = Math.PI * 2;\n/** Smallest absolute angular separation between two radian angles. */\nfunction angularGap(a: number, b: number): number {\n const d = (((a - b) % TWO_PI) + TWO_PI) % TWO_PI;\n return Math.min(d, TWO_PI - d);\n}\n\n/**\n * Back-solve a piece's centroid AND rotation from three card measurements to\n * specific footprint corners — the inverse needed for pieces at non-90° angles,\n * where the card pins three corner-to-edge distances rather than one per axis.\n *\n * Closed form: with the (unknown) rotation θ, each corner `v` resolves to\n * `centroid + R(θ)·v`. Subtracting two same-axis measurements cancels the\n * centroid and leaves `A·cosθ + B·sinθ = C`, solved as `θ = atan2(B,A) ±\n * acos(C/√(A²+B²))`; the root nearest `rotationHint` is chosen. One measurement\n * on each axis then pins the centroid.\n */\nexport function solveCentroidTriangulated(\n input: TriangulateInput,\n): { x: number; y: number; rotation: number } {\n // Mirror-applied, pre-rotation offsets (θ is the unknown we're solving for).\n const offsets = orientedOffsets(input.footprint, 0, input.mirror);\n const items = input.lines.map((l) => {\n const o = offsets[l.vertex];\n if (!o) throw new TerrainSolveError(`vertex index ${l.vertex} out of range`);\n const axis = axisOfEdge(l.edge);\n let target: number;\n switch (l.edge) {\n case \"left\":\n target = l.distance;\n break;\n case \"right\":\n target = input.board.width - l.distance;\n break;\n case \"top\":\n target = l.distance;\n break;\n case \"bottom\":\n target = input.board.height - l.distance;\n break;\n }\n return { axis, target, o };\n });\n const xs = items.filter((i) => i.axis === \"x\");\n const ys = items.filter((i) => i.axis === \"y\");\n\n let pivot: typeof items;\n let pivotAxis: \"x\" | \"y\";\n if (xs.length >= 2 && ys.length >= 1) {\n pivot = xs;\n pivotAxis = \"x\";\n } else if (ys.length >= 2 && xs.length >= 1) {\n pivot = ys;\n pivotAxis = \"y\";\n } else {\n throw new TerrainSolveError(\n \"triangulation needs two measurements from one pair of edges (left/right or top/bottom) and one from the other\",\n );\n }\n\n // Best-conditioned pair on the pivot axis (corners that are furthest apart).\n let a = pivot[0];\n let b = pivot[1];\n let spread = -1;\n for (let i = 0; i < pivot.length; i++) {\n for (let j = i + 1; j < pivot.length; j++) {\n const d = Math.hypot(pivot[i].o.x - pivot[j].o.x, pivot[i].o.y - pivot[j].o.y);\n if (d > spread) {\n spread = d;\n a = pivot[i];\n b = pivot[j];\n }\n }\n }\n\n // Subtract the two same-axis equations → A·cosθ + B·sinθ = C.\n // x-axis vertex eq: cx + (cosθ·o.x − sinθ·o.y) = target\n // y-axis vertex eq: cy + (sinθ·o.x + cosθ·o.y) = target\n const dx = a.o.x - b.o.x;\n const dy = a.o.y - b.o.y;\n const C = a.target - b.target;\n const A = pivotAxis === \"x\" ? dx : dy;\n const B = pivotAxis === \"x\" ? -dy : dx;\n const R = Math.hypot(A, B);\n if (R < 1e-9) {\n throw new TerrainSolveError(\"the two same-edge measurements must reference different corners\");\n }\n const ratio = C / R;\n if (ratio > 1 + 1e-6 || ratio < -1 - 1e-6) {\n throw new TerrainSolveError(\"measurements are inconsistent — no orientation fits\");\n }\n const phi = Math.atan2(B, A);\n const base = Math.acos(Math.max(-1, Math.min(1, ratio)));\n const hint = ((input.rotationHint ?? 0) * Math.PI) / 180;\n const theta = [phi + base, phi - base].reduce((best, c) =>\n angularGap(c, hint) < angularGap(best, hint) ? c : best,\n );\n\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n const xLine = xs[0];\n const yLine = ys[0];\n const x = xLine.target - (cos * xLine.o.x - sin * xLine.o.y);\n const y = yLine.target - (sin * yLine.o.x + cos * yLine.o.y);\n const rotation = (((theta * 180) / Math.PI) % 360 + 360) % 360;\n return { x, y, rotation };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"condition.d.ts","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IAChC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAWD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAwItD"}
1
+ {"version":3,"file":"condition.d.ts","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IAChC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAWD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CA4JtD"}
@@ -100,16 +100,42 @@ export function describeCondition(c) {
100
100
  case "new-objective-controlled":
101
101
  return `${negate}you newly control ${count(p.count_min ?? 1, "objective")} this turn`;
102
102
  case "destroyed-while-on-objective": {
103
+ const obj = p.objective_role ? `a ${dekebab(str(p.objective_role))} objective` : "an objective";
103
104
  let s = `${negate}${count(p.count_min ?? 1, "enemy unit")} destroyed`;
104
105
  if (p.destroyer_on_objective)
105
- s += " by a unit on an objective";
106
+ s += ` by a unit on ${obj}`;
106
107
  if (p.victim_on_objective)
107
- s += " while on an objective";
108
+ s += ` while on ${obj}`;
109
+ if (p.victim_started_turn_on_objective)
110
+ s += ` that started the turn on ${obj}`;
108
111
  return s;
109
112
  }
110
113
  case "destroyed-in-tagged-terrain": {
111
114
  const where = p.at_start_of_turn ? "that started the turn in" : "while in";
112
- return `${negate}${count(p.count_min ?? 1, "enemy unit")} destroyed ${where} ${dekebab(str(p.tag))} terrain`;
115
+ const terrain = p.tag != null ? `${dekebab(str(p.tag))} terrain` : "a terrain area";
116
+ return `${negate}${count(p.count_min ?? 1, "enemy unit")} destroyed ${where} ${terrain}`;
117
+ }
118
+ case "operation-markers": {
119
+ const side = p.side != null ? `${str(p.side)} ` : "";
120
+ const min = typeof p.count_min === "number" ? p.count_min : undefined;
121
+ const max = typeof p.count_max === "number" ? p.count_max : undefined;
122
+ let s;
123
+ if (max === 0) {
124
+ s = `no ${side}operation markers on the battlefield`;
125
+ }
126
+ else if (min != null && max != null && min === max) {
127
+ s = `exactly ${min} ${side}operation marker${min === 1 ? "" : "s"} on the battlefield`;
128
+ }
129
+ else {
130
+ s = `${str(min ?? 1)}+ ${side}operation markers on the battlefield`;
131
+ }
132
+ if (p.within_range_of != null)
133
+ s += ` within range of ${dekebab(str(p.within_range_of))}`;
134
+ if (p.friendly_unit_in_same_terrain_area)
135
+ s += " with a friendly unit in the same terrain area";
136
+ if (p.no_enemy_in_terrain_area)
137
+ s += " and no enemy units in that terrain area";
138
+ return `${negate}${s}`;
113
139
  }
114
140
  case "action-completed": {
115
141
  let s = `${negate}${count(p.count_min ?? 1, "action")} completed`;
@@ -1 +1 @@
1
- {"version":3,"file":"condition.js","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,kFAAkF;AAClF,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,GAAG,CAAC,CAAU;IACrB,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,+EAA+E;AAC/E,SAAS,KAAK,CAAC,CAAU,EAAE,IAAY;IACrC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,CAAY;IAC5C,6EAA6E;IAC7E,6DAA6D;IAC7D,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvC,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACjE,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACvC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;IAE7B,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,2EAA2E;QAC3E,KAAK,UAAU;YACb,OAAO,GAAG,MAAM,cAAc,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;QACrD,KAAK,WAAW;YACd,OAAO,GAAG,MAAM,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACjD,KAAK,gBAAgB;YACnB,OAAO,GAAG,MAAM,MAAM,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,iBAAiB,OAAO,CAAC;QACnI,KAAK,mBAAmB;YACtB,OAAO,GAAG,MAAM,4BAA4B,CAAC;QAC/C,KAAK,oBAAoB;YACvB,OAAO,GAAG,MAAM,6BAA6B,CAAC;QAChD,KAAK,qBAAqB;YACxB,OAAO,GAAG,MAAM,8BAA8B,CAAC;QACjD,KAAK,8BAA8B;YACjC,OAAO,GAAG,MAAM,qCAAqC,CAAC;QACxD,KAAK,0BAA0B;YAC7B,OAAO,GAAG,MAAM,iCAAiC,CAAC;QACpD,KAAK,kBAAkB;YACrB,OAAO,GAAG,MAAM,iBAAiB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QACrD,KAAK,oBAAoB;YACvB,OAAO,GAAG,MAAM,mBAAmB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QACvD,KAAK,iBAAiB;YACpB,OAAO,GAAG,MAAM,6BAA6B,CAAC;QAChD,KAAK,aAAa;YAChB,OAAO,GAAG,MAAM,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAC/E,KAAK,gBAAgB;YACnB,OAAO,GAAG,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC;QACtD,KAAK,mBAAmB;YACtB,OAAO,GAAG,MAAM,4BAA4B,CAAC;QAC/C,KAAK,iBAAiB;YACpB,OAAO,GAAG,MAAM,2BAA2B,CAAC;QAC9C,KAAK,4BAA4B;YAC/B,OAAO,GAAG,MAAM,2BAA2B,CAAC,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAClH,KAAK,sBAAsB;YACzB,OAAO,GAAG,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC3H,KAAK,2BAA2B;YAC9B,OAAO,GAAG,MAAM,8BAA8B,CAAC;QACjD,KAAK,uBAAuB;YAC1B,OAAO,GAAG,MAAM,uBAAuB,CAAC;QAC1C,KAAK,0BAA0B;YAC7B,OAAO,GAAG,MAAM,kBAAkB,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC;QAEhE,2EAA2E;QAC3E,KAAK,oBAAoB;YACvB,OAAO,GAAG,MAAM,qCAAqC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,UAAU,CAAC,CAAC,EAAE,CAAC;QACnG,KAAK,oBAAoB,CAAC,CAAC,CAAC;YAC1B,MAAM,IAAI,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC;YAC5F,IAAI,CAAC,GAAG,GAAG,MAAM,eAAe,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;YAChE,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI;gBAAE,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI;gBAAE,CAAC,IAAI,eAAe,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC;YACtE,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,iBAAiB;YACpB,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC1G,KAAK,4BAA4B,CAAC,CAAC,CAAC;YAClC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAA4B,CAAC;YAC1D,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAA4B,CAAC;YAC3D,MAAM,GAAG,GAAG,CAAC,CAAC,UAAU,KAAK,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC;YAC9E,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;YACjE,OAAO,GAAG,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACzJ,CAAC;QACD,KAAK,0BAA0B;YAC7B,OAAO,GAAG,MAAM,qBAAqB,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC;QACxF,KAAK,8BAA8B,CAAC,CAAC,CAAC;YACpC,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC;YACtE,IAAI,CAAC,CAAC,sBAAsB;gBAAE,CAAC,IAAI,4BAA4B,CAAC;YAChE,IAAI,CAAC,CAAC,mBAAmB;gBAAE,CAAC,IAAI,wBAAwB,CAAC;YACzD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,6BAA6B,CAAC,CAAC,CAAC;YACnC,MAAM,KAAK,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,UAAU,CAAC;YAC3E,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,YAAY,CAAC,cAAc,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC;QAC/G,CAAC;QACD,KAAK,kBAAkB,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC;YAClE,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI;gBAAE,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;YACrE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAA4B,CAAC;YAC9D,IAAI,EAAE,CAAC,cAAc,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC;YAC5E,IAAI,EAAE,CAAC,kBAAkB;gBAAE,CAAC,IAAI,qBAAqB,CAAC;YACtD,IAAI,EAAE,CAAC,OAAO,IAAI,IAAI;gBAAE,CAAC,IAAI,eAAe,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC;YACxE,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI;gBAAE,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACxD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,mBAAmB,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,WAAW,CAAC,WAAW,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACzF,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC;YAC/D,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI;gBAAE,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,WAAW;gBAAE,CAAC,IAAI,yBAAyB,CAAC;YAClD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACnG,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC;YAC1D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,IAAI,CAAC,GAAG,GAAG,MAAM,kBAAkB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,kBAAkB,IAAI,IAAI;gBAAE,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAC5F,IAAI,CAAC,CAAC,eAAe,IAAI,IAAI;gBAAE,CAAC,IAAI,gBAAgB,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,cAAc,CAAC;YACzF,IAAI,CAAC,CAAC,WAAW;gBAAE,CAAC,IAAI,yBAAyB,CAAC;YAClD,IAAI,CAAC,CAAC,WAAW;gBAAE,CAAC,IAAI,+BAA+B,CAAC;YACxD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,sBAAsB;YACzB,OAAO,GAAG,MAAM,mCAAmC,GAAG,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,CAAC;QACtF,KAAK,mBAAmB,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC,GAAG,GAAG,MAAM,eAAe,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,gBAAgB,CAAC,CAAC,EAAE,CAAC;YACpF,IAAI,CAAC,CAAC,eAAe,IAAI,IAAI;gBAAE,CAAC,IAAI,iBAAiB,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,cAAc,CAAC;YAC1F,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,mBAAmB;YACtB,OAAO,GAAG,MAAM,sBAAsB,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,UAAU,CAAC;QAExE;YACE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,SAAS,CAAC,EAAE,CAAC;IACtD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Humanize an Ability-DSL / scoring `condition` into plain English.\n *\n * Shared by the ability-text CLI (`commands/translate.ts`) and the scoring-card\n * translator (`scoring.ts`). Output is **ASCII-only** with a fixed clause and\n * parameter order: it is pinned byte-for-byte across the TS and Rust ports by\n * the `conformance/scoring-translation` corpus, so any phrasing change here is a\n * semantic corpus change (bump `conformance/SPEC_VERSION`).\n */\n\n/**\n * Minimal structural view of a condition node. Matches both the ability-dsl\n * condition schema and the `secondary-card` award `when` field (a simple node\n * carries `type` + `parameters` + `negated`; a compound node carries\n * `operator` + `operands`).\n */\nexport interface Condition {\n type?: string;\n operator?: \"and\" | \"or\" | \"not\";\n operands?: Condition[];\n parameters?: Record<string, unknown>;\n negated?: boolean;\n}\n\n/** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */\nexport function dekebab(s: string): string {\n return s.replace(/-/g, \" \");\n}\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : String(v);\n}\n\n/** `2` + `objective` → `2+ objectives`. Nouns here are all regular plurals. */\nfunction count(n: unknown, noun: string): string {\n return `${str(n)}+ ${noun}s`;\n}\n\nexport function describeCondition(c: Condition): string {\n // Compound nodes first — join the operands with lowercase connectives so the\n // result reads naturally inside a \"... when X and Y\" clause.\n if (c.operator === \"and\" && c.operands) {\n return c.operands.map(describeCondition).join(\" and \");\n }\n if (c.operator === \"or\" && c.operands) {\n return c.operands.map(describeCondition).join(\" or \");\n }\n if (c.operator === \"not\" && c.operands) {\n return `not (${c.operands.map(describeCondition).join(\", \")})`;\n }\n\n const negate = c.negated ? \"not \" : \"\";\n const p = c.parameters ?? {};\n\n switch (c.type) {\n // ── Ability-DSL conditions (ported from commands/translate.ts) ──────────\n case \"phase-is\":\n return `${negate}during the ${str(p.phase)} phase`;\n case \"timing-is\":\n return `${negate}at ${dekebab(str(p.timing))}`;\n case \"player-turn-is\":\n return `${negate}in ${p.turn === \"your-turn\" ? \"your\" : p.turn === \"opponent-turn\" ? \"the opponent's\" : \"either player's\"} turn`;\n case \"charged-this-turn\":\n return `${negate}the unit charged this turn`;\n case \"advanced-this-turn\":\n return `${negate}the unit advanced this turn`;\n case \"remained-stationary\":\n return `${negate}the unit remained stationary`;\n case \"unit-below-starting-strength\":\n return `${negate}the unit is below starting strength`;\n case \"unit-below-half-strength\":\n return `${negate}the unit is below half strength`;\n case \"unit-has-keyword\":\n return `${negate}the unit has \"${str(p.keyword)}\"`;\n case \"target-has-keyword\":\n return `${negate}the target has \"${str(p.keyword)}\"`;\n case \"model-is-leader\":\n return `${negate}the model is leading a unit`;\n case \"is-attached\":\n return `${negate}attached to a ${p.keyword ? `${str(p.keyword)} ` : \"\"}unit`;\n case \"attack-is-type\":\n return `${negate}for ${str(p.attack_type)} attacks`;\n case \"is-battle-shocked\":\n return `${negate}the unit is battle-shocked`;\n case \"has-lost-wounds\":\n return `${negate}the model has lost wounds`;\n case \"opponent-unit-within-range\":\n return `${negate}an enemy unit is within ${p.range === \"engagement\" ? \"engagement range\" : `${str(p.range)}\"`}`;\n case \"unit-within-range-of\":\n return `${negate}within ${str(p.range)}\" of ${str(p.target_type ?? \"target\")}${p.keyword ? ` (${str(p.keyword)})` : \"\"}`;\n case \"within-range-of-objective\":\n return `${negate}within range of an objective`;\n case \"has-fought-this-phase\":\n return `${negate}has fought this phase`;\n case \"destroyed-by-attack-type\":\n return `${negate}destroyed by a ${str(p.attack_type)} attack`;\n\n // ── Scoring conditions (secondary-card award `when`) ────────────────────\n case \"objective-majority\":\n return `${negate}you hold more objectives than the ${dekebab(str(p.relative_to ?? \"opponent\"))}`;\n case \"controls-objective\": {\n const noun = p.objective_role ? `${dekebab(str(p.objective_role))} objective` : \"objective\";\n let s = `${negate}you control ${count(p.count_min ?? 1, noun)}`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.exclude != null) s += ` (excluding ${dekebab(str(p.exclude))})`;\n return s;\n }\n case \"units-destroyed\":\n return `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed ${dekebab(str(p.window))}`;\n case \"units-destroyed-comparison\": {\n const subj = (p.subject ?? {}) as Record<string, unknown>;\n const ref = (p.reference ?? {}) as Record<string, unknown>;\n const cmp = p.comparator === \"greater-or-equal\" ? \"at least as many\" : \"more\";\n const link = p.comparator === \"greater-or-equal\" ? \"as\" : \"than\";\n return `${negate}you destroyed ${cmp} ${str(subj.side)} units ${dekebab(str(subj.window))} ${link} ${str(ref.side)} units ${dekebab(str(ref.window))}`;\n }\n case \"new-objective-controlled\":\n return `${negate}you newly control ${count(p.count_min ?? 1, \"objective\")} this turn`;\n case \"destroyed-while-on-objective\": {\n let s = `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed`;\n if (p.destroyer_on_objective) s += \" by a unit on an objective\";\n if (p.victim_on_objective) s += \" while on an objective\";\n return s;\n }\n case \"destroyed-in-tagged-terrain\": {\n const where = p.at_start_of_turn ? \"that started the turn in\" : \"while in\";\n return `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed ${where} ${dekebab(str(p.tag))} terrain`;\n }\n case \"action-completed\": {\n let s = `${negate}${count(p.count_min ?? 1, \"action\")} completed`;\n if (p.action_id != null) s += ` (${dekebab(str(p.action_id))})`;\n if (p.target_kind != null) s += ` on ${dekebab(str(p.target_kind))}`;\n const tf = (p.target_filter ?? {}) as Record<string, unknown>;\n if (tf.objective_role != null) s += ` (${dekebab(str(tf.objective_role))})`;\n if (tf.in_enemy_territory) s += \" in enemy territory\";\n if (tf.exclude != null) s += ` (excluding ${dekebab(str(tf.exclude))})`;\n if (p.window != null) s += ` ${dekebab(str(p.window))}`;\n return s;\n }\n case \"objective-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, \"objective\")} tagged ${dekebab(str(p.tag))}`;\n if (p.count_max != null) s += ` (at most ${str(p.count_max)})`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.last_marked) s += \" (most recently marked)\";\n return s;\n }\n case \"unit-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} tagged ${dekebab(str(p.tag))}`;\n if (p.window != null) s += ` (${dekebab(str(p.window))})`;\n return s;\n }\n case \"terrain-has-tag\": {\n let s = `${negate}terrain tagged ${dekebab(str(p.tag))}`;\n if (p.friendly_units_min != null) s += ` with ${str(p.friendly_units_min)}+ friendly units`;\n if (p.enemy_units_max != null) s += ` and at most ${str(p.enemy_units_max)} enemy units`;\n if (p.last_marked) s += \" (most recently marked)\";\n if (p.in_enemy_dz) s += \" in the enemy deployment zone\";\n return s;\n }\n case \"terrain-area-control\":\n return `${negate}you control a terrain area with ${str(p.min_models ?? 1)}+ models`;\n case \"territory-control\": {\n let s = `${negate}you control ${dekebab(str(p.territory_ref ?? \"your-territory\"))}`;\n if (p.enemy_units_max != null) s += ` with at most ${str(p.enemy_units_max)} enemy units`;\n return s;\n }\n case \"engagement-fronts\":\n return `${negate}you are engaged on ${str(p.count_min ?? 1)}+ fronts`;\n\n default:\n return `${negate}${dekebab(c.type ?? \"unknown\")}`;\n }\n}\n"]}
1
+ {"version":3,"file":"condition.js","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,kFAAkF;AAClF,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,GAAG,CAAC,CAAU;IACrB,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,+EAA+E;AAC/E,SAAS,KAAK,CAAC,CAAU,EAAE,IAAY;IACrC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,CAAY;IAC5C,6EAA6E;IAC7E,6DAA6D;IAC7D,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvC,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACjE,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACvC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;IAE7B,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,2EAA2E;QAC3E,KAAK,UAAU;YACb,OAAO,GAAG,MAAM,cAAc,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;QACrD,KAAK,WAAW;YACd,OAAO,GAAG,MAAM,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACjD,KAAK,gBAAgB;YACnB,OAAO,GAAG,MAAM,MAAM,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,iBAAiB,OAAO,CAAC;QACnI,KAAK,mBAAmB;YACtB,OAAO,GAAG,MAAM,4BAA4B,CAAC;QAC/C,KAAK,oBAAoB;YACvB,OAAO,GAAG,MAAM,6BAA6B,CAAC;QAChD,KAAK,qBAAqB;YACxB,OAAO,GAAG,MAAM,8BAA8B,CAAC;QACjD,KAAK,8BAA8B;YACjC,OAAO,GAAG,MAAM,qCAAqC,CAAC;QACxD,KAAK,0BAA0B;YAC7B,OAAO,GAAG,MAAM,iCAAiC,CAAC;QACpD,KAAK,kBAAkB;YACrB,OAAO,GAAG,MAAM,iBAAiB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QACrD,KAAK,oBAAoB;YACvB,OAAO,GAAG,MAAM,mBAAmB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QACvD,KAAK,iBAAiB;YACpB,OAAO,GAAG,MAAM,6BAA6B,CAAC;QAChD,KAAK,aAAa;YAChB,OAAO,GAAG,MAAM,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAC/E,KAAK,gBAAgB;YACnB,OAAO,GAAG,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC;QACtD,KAAK,mBAAmB;YACtB,OAAO,GAAG,MAAM,4BAA4B,CAAC;QAC/C,KAAK,iBAAiB;YACpB,OAAO,GAAG,MAAM,2BAA2B,CAAC;QAC9C,KAAK,4BAA4B;YAC/B,OAAO,GAAG,MAAM,2BAA2B,CAAC,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAClH,KAAK,sBAAsB;YACzB,OAAO,GAAG,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC3H,KAAK,2BAA2B;YAC9B,OAAO,GAAG,MAAM,8BAA8B,CAAC;QACjD,KAAK,uBAAuB;YAC1B,OAAO,GAAG,MAAM,uBAAuB,CAAC;QAC1C,KAAK,0BAA0B;YAC7B,OAAO,GAAG,MAAM,kBAAkB,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC;QAEhE,2EAA2E;QAC3E,KAAK,oBAAoB;YACvB,OAAO,GAAG,MAAM,qCAAqC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,UAAU,CAAC,CAAC,EAAE,CAAC;QACnG,KAAK,oBAAoB,CAAC,CAAC,CAAC;YAC1B,MAAM,IAAI,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC;YAC5F,IAAI,CAAC,GAAG,GAAG,MAAM,eAAe,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;YAChE,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI;gBAAE,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI;gBAAE,CAAC,IAAI,eAAe,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC;YACtE,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,iBAAiB;YACpB,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC1G,KAAK,4BAA4B,CAAC,CAAC,CAAC;YAClC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAA4B,CAAC;YAC1D,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAA4B,CAAC;YAC3D,MAAM,GAAG,GAAG,CAAC,CAAC,UAAU,KAAK,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC;YAC9E,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;YACjE,OAAO,GAAG,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACzJ,CAAC;QACD,KAAK,0BAA0B;YAC7B,OAAO,GAAG,MAAM,qBAAqB,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC;QACxF,KAAK,8BAA8B,CAAC,CAAC,CAAC;YACpC,MAAM,GAAG,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,CAAC;YAChG,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC;YACtE,IAAI,CAAC,CAAC,sBAAsB;gBAAE,CAAC,IAAI,iBAAiB,GAAG,EAAE,CAAC;YAC1D,IAAI,CAAC,CAAC,mBAAmB;gBAAE,CAAC,IAAI,aAAa,GAAG,EAAE,CAAC;YACnD,IAAI,CAAC,CAAC,gCAAgC;gBAAE,CAAC,IAAI,6BAA6B,GAAG,EAAE,CAAC;YAChF,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,6BAA6B,CAAC,CAAC,CAAC;YACnC,MAAM,KAAK,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,UAAU,CAAC;YAC3E,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAC;YACpF,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,YAAY,CAAC,cAAc,KAAK,IAAI,OAAO,EAAE,CAAC;QAC3F,CAAC;QACD,KAAK,mBAAmB,CAAC,CAAC,CAAC;YACzB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YACtE,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YACtE,IAAI,CAAS,CAAC;YACd,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;gBACd,CAAC,GAAG,MAAM,IAAI,sCAAsC,CAAC;YACvD,CAAC;iBAAM,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;gBACrD,CAAC,GAAG,WAAW,GAAG,IAAI,IAAI,mBAAmB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC;YACzF,CAAC;iBAAM,CAAC;gBACN,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,IAAI,sCAAsC,CAAC;YACtE,CAAC;YACD,IAAI,CAAC,CAAC,eAAe,IAAI,IAAI;gBAAE,CAAC,IAAI,oBAAoB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC;YAC1F,IAAI,CAAC,CAAC,kCAAkC;gBAAE,CAAC,IAAI,gDAAgD,CAAC;YAChG,IAAI,CAAC,CAAC,wBAAwB;gBAAE,CAAC,IAAI,0CAA0C,CAAC;YAChF,OAAO,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,CAAC;QACD,KAAK,kBAAkB,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC;YAClE,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI;gBAAE,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;YACrE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAA4B,CAAC;YAC9D,IAAI,EAAE,CAAC,cAAc,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC;YAC5E,IAAI,EAAE,CAAC,kBAAkB;gBAAE,CAAC,IAAI,qBAAqB,CAAC;YACtD,IAAI,EAAE,CAAC,OAAO,IAAI,IAAI;gBAAE,CAAC,IAAI,eAAe,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC;YACxE,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI;gBAAE,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACxD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,mBAAmB,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,WAAW,CAAC,WAAW,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACzF,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC;YAC/D,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI;gBAAE,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,WAAW;gBAAE,CAAC,IAAI,yBAAyB,CAAC;YAClD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,IAAI,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACnG,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI;gBAAE,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC;YAC1D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,IAAI,CAAC,GAAG,GAAG,MAAM,kBAAkB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,kBAAkB,IAAI,IAAI;gBAAE,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAC5F,IAAI,CAAC,CAAC,eAAe,IAAI,IAAI;gBAAE,CAAC,IAAI,gBAAgB,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,cAAc,CAAC;YACzF,IAAI,CAAC,CAAC,WAAW;gBAAE,CAAC,IAAI,yBAAyB,CAAC;YAClD,IAAI,CAAC,CAAC,WAAW;gBAAE,CAAC,IAAI,+BAA+B,CAAC;YACxD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,sBAAsB;YACzB,OAAO,GAAG,MAAM,mCAAmC,GAAG,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,CAAC;QACtF,KAAK,mBAAmB,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC,GAAG,GAAG,MAAM,eAAe,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,gBAAgB,CAAC,CAAC,EAAE,CAAC;YACpF,IAAI,CAAC,CAAC,eAAe,IAAI,IAAI;gBAAE,CAAC,IAAI,iBAAiB,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,cAAc,CAAC;YAC1F,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,mBAAmB;YACtB,OAAO,GAAG,MAAM,sBAAsB,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,UAAU,CAAC;QAExE;YACE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,SAAS,CAAC,EAAE,CAAC;IACtD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Humanize an Ability-DSL / scoring `condition` into plain English.\n *\n * Shared by the ability-text CLI (`commands/translate.ts`) and the scoring-card\n * translator (`scoring.ts`). Output is **ASCII-only** with a fixed clause and\n * parameter order: it is pinned byte-for-byte across the TS and Rust ports by\n * the `conformance/scoring-translation` corpus, so any phrasing change here is a\n * semantic corpus change (bump `conformance/SPEC_VERSION`).\n */\n\n/**\n * Minimal structural view of a condition node. Matches both the ability-dsl\n * condition schema and the `secondary-card` award `when` field (a simple node\n * carries `type` + `parameters` + `negated`; a compound node carries\n * `operator` + `operands`).\n */\nexport interface Condition {\n type?: string;\n operator?: \"and\" | \"or\" | \"not\";\n operands?: Condition[];\n parameters?: Record<string, unknown>;\n negated?: boolean;\n}\n\n/** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */\nexport function dekebab(s: string): string {\n return s.replace(/-/g, \" \");\n}\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : String(v);\n}\n\n/** `2` + `objective` → `2+ objectives`. Nouns here are all regular plurals. */\nfunction count(n: unknown, noun: string): string {\n return `${str(n)}+ ${noun}s`;\n}\n\nexport function describeCondition(c: Condition): string {\n // Compound nodes first — join the operands with lowercase connectives so the\n // result reads naturally inside a \"... when X and Y\" clause.\n if (c.operator === \"and\" && c.operands) {\n return c.operands.map(describeCondition).join(\" and \");\n }\n if (c.operator === \"or\" && c.operands) {\n return c.operands.map(describeCondition).join(\" or \");\n }\n if (c.operator === \"not\" && c.operands) {\n return `not (${c.operands.map(describeCondition).join(\", \")})`;\n }\n\n const negate = c.negated ? \"not \" : \"\";\n const p = c.parameters ?? {};\n\n switch (c.type) {\n // ── Ability-DSL conditions (ported from commands/translate.ts) ──────────\n case \"phase-is\":\n return `${negate}during the ${str(p.phase)} phase`;\n case \"timing-is\":\n return `${negate}at ${dekebab(str(p.timing))}`;\n case \"player-turn-is\":\n return `${negate}in ${p.turn === \"your-turn\" ? \"your\" : p.turn === \"opponent-turn\" ? \"the opponent's\" : \"either player's\"} turn`;\n case \"charged-this-turn\":\n return `${negate}the unit charged this turn`;\n case \"advanced-this-turn\":\n return `${negate}the unit advanced this turn`;\n case \"remained-stationary\":\n return `${negate}the unit remained stationary`;\n case \"unit-below-starting-strength\":\n return `${negate}the unit is below starting strength`;\n case \"unit-below-half-strength\":\n return `${negate}the unit is below half strength`;\n case \"unit-has-keyword\":\n return `${negate}the unit has \"${str(p.keyword)}\"`;\n case \"target-has-keyword\":\n return `${negate}the target has \"${str(p.keyword)}\"`;\n case \"model-is-leader\":\n return `${negate}the model is leading a unit`;\n case \"is-attached\":\n return `${negate}attached to a ${p.keyword ? `${str(p.keyword)} ` : \"\"}unit`;\n case \"attack-is-type\":\n return `${negate}for ${str(p.attack_type)} attacks`;\n case \"is-battle-shocked\":\n return `${negate}the unit is battle-shocked`;\n case \"has-lost-wounds\":\n return `${negate}the model has lost wounds`;\n case \"opponent-unit-within-range\":\n return `${negate}an enemy unit is within ${p.range === \"engagement\" ? \"engagement range\" : `${str(p.range)}\"`}`;\n case \"unit-within-range-of\":\n return `${negate}within ${str(p.range)}\" of ${str(p.target_type ?? \"target\")}${p.keyword ? ` (${str(p.keyword)})` : \"\"}`;\n case \"within-range-of-objective\":\n return `${negate}within range of an objective`;\n case \"has-fought-this-phase\":\n return `${negate}has fought this phase`;\n case \"destroyed-by-attack-type\":\n return `${negate}destroyed by a ${str(p.attack_type)} attack`;\n\n // ── Scoring conditions (secondary-card award `when`) ────────────────────\n case \"objective-majority\":\n return `${negate}you hold more objectives than the ${dekebab(str(p.relative_to ?? \"opponent\"))}`;\n case \"controls-objective\": {\n const noun = p.objective_role ? `${dekebab(str(p.objective_role))} objective` : \"objective\";\n let s = `${negate}you control ${count(p.count_min ?? 1, noun)}`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.exclude != null) s += ` (excluding ${dekebab(str(p.exclude))})`;\n return s;\n }\n case \"units-destroyed\":\n return `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed ${dekebab(str(p.window))}`;\n case \"units-destroyed-comparison\": {\n const subj = (p.subject ?? {}) as Record<string, unknown>;\n const ref = (p.reference ?? {}) as Record<string, unknown>;\n const cmp = p.comparator === \"greater-or-equal\" ? \"at least as many\" : \"more\";\n const link = p.comparator === \"greater-or-equal\" ? \"as\" : \"than\";\n return `${negate}you destroyed ${cmp} ${str(subj.side)} units ${dekebab(str(subj.window))} ${link} ${str(ref.side)} units ${dekebab(str(ref.window))}`;\n }\n case \"new-objective-controlled\":\n return `${negate}you newly control ${count(p.count_min ?? 1, \"objective\")} this turn`;\n case \"destroyed-while-on-objective\": {\n const obj = p.objective_role ? `a ${dekebab(str(p.objective_role))} objective` : \"an objective\";\n let s = `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed`;\n if (p.destroyer_on_objective) s += ` by a unit on ${obj}`;\n if (p.victim_on_objective) s += ` while on ${obj}`;\n if (p.victim_started_turn_on_objective) s += ` that started the turn on ${obj}`;\n return s;\n }\n case \"destroyed-in-tagged-terrain\": {\n const where = p.at_start_of_turn ? \"that started the turn in\" : \"while in\";\n const terrain = p.tag != null ? `${dekebab(str(p.tag))} terrain` : \"a terrain area\";\n return `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed ${where} ${terrain}`;\n }\n case \"operation-markers\": {\n const side = p.side != null ? `${str(p.side)} ` : \"\";\n const min = typeof p.count_min === \"number\" ? p.count_min : undefined;\n const max = typeof p.count_max === \"number\" ? p.count_max : undefined;\n let s: string;\n if (max === 0) {\n s = `no ${side}operation markers on the battlefield`;\n } else if (min != null && max != null && min === max) {\n s = `exactly ${min} ${side}operation marker${min === 1 ? \"\" : \"s\"} on the battlefield`;\n } else {\n s = `${str(min ?? 1)}+ ${side}operation markers on the battlefield`;\n }\n if (p.within_range_of != null) s += ` within range of ${dekebab(str(p.within_range_of))}`;\n if (p.friendly_unit_in_same_terrain_area) s += \" with a friendly unit in the same terrain area\";\n if (p.no_enemy_in_terrain_area) s += \" and no enemy units in that terrain area\";\n return `${negate}${s}`;\n }\n case \"action-completed\": {\n let s = `${negate}${count(p.count_min ?? 1, \"action\")} completed`;\n if (p.action_id != null) s += ` (${dekebab(str(p.action_id))})`;\n if (p.target_kind != null) s += ` on ${dekebab(str(p.target_kind))}`;\n const tf = (p.target_filter ?? {}) as Record<string, unknown>;\n if (tf.objective_role != null) s += ` (${dekebab(str(tf.objective_role))})`;\n if (tf.in_enemy_territory) s += \" in enemy territory\";\n if (tf.exclude != null) s += ` (excluding ${dekebab(str(tf.exclude))})`;\n if (p.window != null) s += ` ${dekebab(str(p.window))}`;\n return s;\n }\n case \"objective-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, \"objective\")} tagged ${dekebab(str(p.tag))}`;\n if (p.count_max != null) s += ` (at most ${str(p.count_max)})`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.last_marked) s += \" (most recently marked)\";\n return s;\n }\n case \"unit-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} tagged ${dekebab(str(p.tag))}`;\n if (p.window != null) s += ` (${dekebab(str(p.window))})`;\n return s;\n }\n case \"terrain-has-tag\": {\n let s = `${negate}terrain tagged ${dekebab(str(p.tag))}`;\n if (p.friendly_units_min != null) s += ` with ${str(p.friendly_units_min)}+ friendly units`;\n if (p.enemy_units_max != null) s += ` and at most ${str(p.enemy_units_max)} enemy units`;\n if (p.last_marked) s += \" (most recently marked)\";\n if (p.in_enemy_dz) s += \" in the enemy deployment zone\";\n return s;\n }\n case \"terrain-area-control\":\n return `${negate}you control a terrain area with ${str(p.min_models ?? 1)}+ models`;\n case \"territory-control\": {\n let s = `${negate}you control ${dekebab(str(p.territory_ref ?? \"your-territory\"))}`;\n if (p.enemy_units_max != null) s += ` with at most ${str(p.enemy_units_max)} enemy units`;\n return s;\n }\n case \"engagement-fronts\":\n return `${negate}you are engaged on ${str(p.count_min ?? 1)}+ fronts`;\n\n default:\n return `${negate}${dekebab(c.type ?? \"unknown\")}`;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpaca-software/40kdc-data",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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": [
@@ -138,6 +138,21 @@
138
138
  "$ref": "../defs/common.schema.json#/$defs/phase",
139
139
  "description": "Phase in which the action can be started."
140
140
  },
141
+ "timing": {
142
+ "type": "string",
143
+ "enum": ["start-of-battle", "start-of-turn", "end-of-turn"],
144
+ "description": "Non-phase moment the action happens, for card rules that are not started in a phase (Locate and Deny's start-of-battle marker placement, Punishment's start-of-turn condemnation, Consecrate's end-of-turn objective selection). Mutually informative with `starts` — a card action uses one or the other."
145
+ },
146
+ "battle_round": {
147
+ "type": "object",
148
+ "description": "Battle-round window in which the action can be started. Absent means any battle round. 'From the second battle round onwards' (Triangulate, Extract Intelligence) is { min: 2 }.",
149
+ "properties": {
150
+ "min": { "type": "integer", "minimum": 1, "maximum": 5 },
151
+ "max": { "type": "integer", "minimum": 1, "maximum": 5 }
152
+ },
153
+ "minProperties": 1,
154
+ "additionalProperties": false
155
+ },
141
156
  "player_turn": { "$ref": "../defs/common.schema.json#/$defs/player-turn" },
142
157
  "units": {
143
158
  "$ref": "../enrichment/ability-dsl/condition.schema.json",
@@ -161,6 +176,10 @@
161
176
  "effect": {
162
177
  "$ref": "../enrichment/ability-dsl/effect.schema.json",
163
178
  "description": "Effect applied when the action completes (e.g. terrain-area-tag, objective-tag, or unit-tag to mark transient state)."
179
+ },
180
+ "restrictions": {
181
+ "$ref": "../enrichment/ability-dsl/condition.schema.json",
182
+ "description": "Predicate that BLOCKS starting the action while it holds (Sensor Sweep: a unit cannot start this action if there is only one operation marker on the battlefield)."
164
183
  }
165
184
  },
166
185
  "additionalProperties": false
@@ -96,6 +96,45 @@
96
96
  }
97
97
  },
98
98
  "additionalProperties": false
99
+ },
100
+ "keystones": {
101
+ "type": "array",
102
+ "description": "Measurement keystones: the author-selected dimension lines a reference card prints so a player can place this piece with a tape measure (board edge → a feature of the placed piece). Only the selection is stored — the distance is always DERIVED from the resolved geometry by the shared keystone resolver (pinned by the conformance corpus), so a keystone can never disagree with the layout. Vertex indices follow the resolver's pinned vertex order; re-authoring a template's footprint invalidates them, so review keystones when geometry changes.",
103
+ "items": {
104
+ "type": "object",
105
+ "properties": {
106
+ "edge": {
107
+ "type": "string",
108
+ "enum": ["left", "right", "top", "bottom"],
109
+ "description": "The board edge the measurement runs from, in the y-down board frame (left/right pin x against board width; top/bottom pin y against board height)."
110
+ },
111
+ "ref": {
112
+ "description": "Which feature of the placed piece the measurement reaches: a footprint vertex (by resolver vertex order) or an axis-aligned bounding face of the placed footprint.",
113
+ "oneOf": [
114
+ {
115
+ "type": "object",
116
+ "properties": {
117
+ "kind": { "const": "vertex" },
118
+ "index": { "type": "integer", "minimum": 0 }
119
+ },
120
+ "required": ["kind", "index"],
121
+ "additionalProperties": false
122
+ },
123
+ {
124
+ "type": "object",
125
+ "properties": {
126
+ "kind": { "const": "face" },
127
+ "side": { "type": "string", "enum": ["min-x", "max-x", "min-y", "max-y"] }
128
+ },
129
+ "required": ["kind", "side"],
130
+ "additionalProperties": false
131
+ }
132
+ ]
133
+ }
134
+ },
135
+ "required": ["edge", "ref"],
136
+ "additionalProperties": false
137
+ }
99
138
  }
100
139
  },
101
140
  "required": ["position"],
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "simple-condition": {
13
13
  "type": "object",
14
- "$comment": "Board/meta-state and scoring predicates. `parameters` is intentionally open (additionalProperties: true); each type documents its own param convention. Scoring predicates added for mission cards: `units-destroyed` { side: 'enemy'|'friendly', window: 'this-turn'|'previous-turn', count_min: int } — at least count_min units of `side` were destroyed in `window`. `units-destroyed-comparison` { subject: {side, window}, comparator: 'greater-than'|'greater-or-equal', reference: {side, window} } — compares two destruction tallies (e.g. more enemy units destroyed this turn than friendly last turn). `objective-majority` { relative_to: 'opponent' } — you control more objectives than the named party. `controls-objective` params: { count_min: int, objective_role?: 'central'|'expansion'|'non-home'|'home', exclude?: 'home', objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' }. Mission-card extensions (11e primary deck): `action-completed` { action_id?: string, target_kind?: 'objective'|'terrain'|'enemy-unit'|'self', target_filter?: { in_enemy_territory?: bool, objective_role?: 'central'|'non-home', exclude?: 'home' }, count_min: int, window?: 'this-turn'|'previous-turn'|'cumulative' } — at least count_min instances of a named action were completed in the window. `objective-has-tag` { tag: 'baited'|'cleansed'|'triangulated'|'consecrated'|'sabotaged'|'marked'|'vanguard'|'spotted', count_min: int, count_max?: int, objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' } — at least count_min objectives carry the named transient tag. `unit-has-tag` { tag: 'doomed'|'spotted', side: 'enemy'|'friendly', count_min: int, window?: 'destroyed-this-turn'|'still-on-board' } — at least count_min units of `side` carry the tag (optionally with a destruction filter — Punishment scores when a Doomed unit was destroyed or left the battlefield). `terrain-has-tag` { tag: 'mined'|'marked'|'vanguard'|'plundered', friendly_units_min?: int, enemy_units_max?: int, last_marked?: bool, in_enemy_dz?: bool } — terrain piece state predicate; `last_marked` selects the most-recently-marked piece (Find and Deny / Recover the Relics' Overwhelming Force trigger). `new-objective-controlled` { count_min: int } — at least count_min objectives are controlled this turn that were not controlled in the previous command phase. `engagement-fronts` { count_min: int } — friendly units engage enemies in at least count_min distinct fronts; a 'front' is one of the four table quarters (board quadrants about the board's centre — each of the four areas formed by dividing the table along both centre lines). `destroyed-while-on-objective` { destroyer_on_objective?: bool, victim_on_objective?: bool, count_min: int } — count_min enemy units were destroyed this turn under the named spatial condition (the destroying friendly unit, the destroyed enemy unit, or both were standing on an objective at the moment of the kill). `destroyed-in-tagged-terrain` { tag: 'mined'|'marked'|'vanguard'|'plundered', at_start_of_turn?: bool, count_min: int } — count_min enemy units were destroyed this turn while in terrain carrying the named tag; with `at_start_of_turn` the victim must have been in that terrain at the start of the turn (Death Trap's Disruption kill bonus), otherwise the spatial test is at the moment of the kill (parallels `destroyed-while-on-objective`).",
14
+ "$comment": "Board/meta-state and scoring predicates. `parameters` is intentionally open (additionalProperties: true); each type documents its own param convention. Scoring predicates added for mission cards: `units-destroyed` { side: 'enemy'|'friendly', window: 'this-turn'|'previous-turn', count_min: int } — at least count_min units of `side` were destroyed in `window`. `units-destroyed-comparison` { subject: {side, window}, comparator: 'greater-than'|'greater-or-equal', reference: {side, window} } — compares two destruction tallies (e.g. more enemy units destroyed this turn than friendly last turn). `objective-majority` { relative_to: 'opponent' } — you control more objectives than the named party. `controls-objective` params: { count_min: int, objective_role?: 'central'|'expansion'|'non-home'|'home', exclude?: 'home', objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' }. Mission-card extensions (11e primary deck): `action-completed` { action_id?: string, target_kind?: 'objective'|'terrain'|'enemy-unit'|'self', target_filter?: { in_enemy_territory?: bool, objective_role?: 'central'|'non-home', exclude?: 'home' }, count_min: int, window?: 'this-turn'|'previous-turn'|'cumulative' } — at least count_min instances of a named action were completed in the window. `objective-has-tag` { tag: 'baited'|'decoyed'|'cleansed'|'triangulated'|'consecrated'|'sabotaged'|'marked'|'vanguard'|'spotted', count_min: int, count_max?: int, objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' } — at least count_min objectives carry the named transient tag. `unit-has-tag` { tag: 'doomed'|'condemned'|'spotted'|'surveilled', side: 'enemy'|'friendly', count_min: int, window?: 'destroyed-this-turn'|'left-battlefield-this-turn'|'this-turn'|'still-on-board' } — at least count_min units of `side` carry the tag (optionally with an event window — Punishment scores when a condemned unit was destroyed or left the battlefield this turn; Surveil the Foe scores on units tagged this turn). `terrain-has-tag` { tag: 'mined'|'trapped'|'marked'|'vanguard'|'plundered', friendly_units_min?: int, enemy_units_max?: int, last_marked?: bool, in_enemy_dz?: bool } — terrain piece state predicate; `last_marked` selects the most-recently-marked piece (Find and Deny / Recover the Relics' Overwhelming Force trigger). `new-objective-controlled` { count_min: int } — at least count_min objectives are controlled this turn that were not controlled in the previous command phase. `engagement-fronts` { count_min: int } — friendly units engage enemies in at least count_min distinct fronts; a 'front' is one of the four table quarters (board quadrants about the board's centre — each of the four areas formed by dividing the table along both centre lines). `destroyed-while-on-objective` { destroyer_on_objective?: bool, victim_on_objective?: bool, victim_started_turn_on_objective?: bool, objective_role?: 'central', count_min: int } — count_min enemy units were destroyed this turn under the named spatial condition (the destroying friendly unit, the destroyed enemy unit, or both were within range of an objective at the moment of the kill; `victim_started_turn_on_objective` instead tests the victim's position at the start of the turn, and `objective_role` narrows which objectives count — Secure Asset's central-objective kill row). `destroyed-in-tagged-terrain` { tag?: 'mined'|'trapped'|'marked'|'vanguard'|'plundered', at_start_of_turn?: bool, count_min: int } — count_min enemy units were destroyed this turn while in terrain carrying the named tag; with `at_start_of_turn` the victim must have been in that terrain at the start of the turn (Death Trap's kill bonus), otherwise the spatial test is at the moment of the kill (parallels `destroyed-while-on-objective`). With `tag` omitted, any terrain area qualifies (Search and Scour). `operation-markers` { side?: 'friendly'|'opponent', count_min?: int, count_max?: int, within_range_of?: 'opponent-home-objective', friendly_unit_in_same_terrain_area?: bool, no_enemy_in_terrain_area?: bool } — counts operation markers on the battlefield (side omitted counts both sides' markers); count_max: 0 is 'none remain', count_min == count_max == 1 is 'exactly one'; the terrain-area flags add the co-location proviso used by Locate and Deny / Extract Relic ('one of your units is within the same terrain area as that marker, and no enemy units are within it').",
15
15
  "properties": {
16
16
  "type": {
17
17
  "type": "string",
@@ -31,7 +31,7 @@
31
31
  "action-completed", "objective-has-tag", "unit-has-tag",
32
32
  "terrain-has-tag", "new-objective-controlled",
33
33
  "engagement-fronts", "destroyed-while-on-objective",
34
- "destroyed-in-tagged-terrain"
34
+ "destroyed-in-tagged-terrain", "operation-markers"
35
35
  ]
36
36
  },
37
37
  "parameters": { "type": "object", "additionalProperties": true },