@alpaca-software/40kdc-data 0.5.4 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/codegen-data.js +1 -0
  2. package/dist/codegen-data.js.map +1 -1
  3. package/dist/compare.d.ts +167 -0
  4. package/dist/compare.d.ts.map +1 -0
  5. package/dist/compare.js +302 -0
  6. package/dist/compare.js.map +1 -0
  7. package/dist/cruncher/buffs.d.ts +13 -0
  8. package/dist/cruncher/buffs.d.ts.map +1 -1
  9. package/dist/cruncher/buffs.js +7 -0
  10. package/dist/cruncher/buffs.js.map +1 -1
  11. package/dist/cruncher/engine.d.ts.map +1 -1
  12. package/dist/cruncher/engine.js +14 -10
  13. package/dist/cruncher/engine.js.map +1 -1
  14. package/dist/data/bundle.generated.js +1 -1
  15. package/dist/data/bundle.generated.js.map +1 -1
  16. package/dist/data/dataset.d.ts +2 -1
  17. package/dist/data/dataset.d.ts.map +1 -1
  18. package/dist/data/dataset.js +2 -0
  19. package/dist/data/dataset.js.map +1 -1
  20. package/dist/data/entities.d.ts.map +1 -1
  21. package/dist/data/entities.js +17 -1
  22. package/dist/data/entities.js.map +1 -1
  23. package/dist/data/index.d.ts +2 -0
  24. package/dist/data/index.d.ts.map +1 -1
  25. package/dist/data/index.js +2 -0
  26. package/dist/data/index.js.map +1 -1
  27. package/dist/data/types.d.ts +3 -1
  28. package/dist/data/types.d.ts.map +1 -1
  29. package/dist/data/types.js +1 -0
  30. package/dist/data/types.js.map +1 -1
  31. package/dist/generated.d.ts +24 -0
  32. package/dist/generated.d.ts.map +1 -1
  33. package/dist/generated.js.map +1 -1
  34. package/dist/import/import-roster.d.ts.map +1 -1
  35. package/dist/import/import-roster.js +5 -0
  36. package/dist/import/import-roster.js.map +1 -1
  37. package/dist/import/index.d.ts +1 -0
  38. package/dist/import/index.d.ts.map +1 -1
  39. package/dist/import/index.js +1 -0
  40. package/dist/import/index.js.map +1 -1
  41. package/dist/import/newrecruit-wtc.d.ts +5 -0
  42. package/dist/import/newrecruit-wtc.d.ts.map +1 -1
  43. package/dist/import/newrecruit-wtc.js +22 -1
  44. package/dist/import/newrecruit-wtc.js.map +1 -1
  45. package/dist/import/roster-json.d.ts +30 -0
  46. package/dist/import/roster-json.d.ts.map +1 -0
  47. package/dist/import/roster-json.js +88 -0
  48. package/dist/import/roster-json.js.map +1 -0
  49. package/dist/import/types.d.ts +1 -1
  50. package/dist/import/types.d.ts.map +1 -1
  51. package/dist/import/types.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/migrate-keystone-twins.d.ts +23 -0
  57. package/dist/migrate-keystone-twins.d.ts.map +1 -0
  58. package/dist/migrate-keystone-twins.js +184 -0
  59. package/dist/migrate-keystone-twins.js.map +1 -0
  60. package/dist/runner.d.ts.map +1 -1
  61. package/dist/runner.js +61 -0
  62. package/dist/runner.js.map +1 -1
  63. package/dist/validate.d.ts.map +1 -1
  64. package/dist/validate.js +1 -0
  65. package/dist/validate.js.map +1 -1
  66. package/package.json +1 -1
  67. package/schemas/core/roster.schema.json +2 -1
  68. package/schemas/core/target-profile.schema.json +30 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Keystone twin pairing for `data/core/terrain-layouts.json`: every authored
3
+ * measurement keystone should have its point-reflected counterpart on the
4
+ * piece's 180°-symmetry twin, so a layout card prints dimension lines for
5
+ * BOTH players' halves (the layout editor now maintains this invariant on
6
+ * add/remove; this script back-fills the existing data and re-audits it).
7
+ *
8
+ * Twins are not stored — they are recovered the way the editor's
9
+ * `autoPairTwins` does: same template, unparented, centroid within 0.75″ of
10
+ * the point reflection through the board centre. The mirrored keystone flips
11
+ * its board edge (left↔right, top↔bottom); a face ref flips its side, and a
12
+ * vertex ref is resolved GEOMETRICALLY — reflect the source vertex through
13
+ * the centre and take the twin's nearest vertex. Index arithmetic is not
14
+ * safe: many layouts store the twin at the same angle (not θ+180), so vertex
15
+ * i on one side need not be vertex i on the other. A reflection that lands
16
+ * more than 0.25″ from every twin vertex is skipped with a warning rather
17
+ * than guessed.
18
+ *
19
+ * Usage (from `tools/`):
20
+ * npx tsx src/migrate-keystone-twins.ts # write missing mirrors
21
+ * npx tsx src/migrate-keystone-twins.ts --check # report only; exit 1 if any are missing
22
+ */
23
+ import { readFileSync, writeFileSync } from "node:fs";
24
+ import { join, resolve } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { orientedOffsets, } from "./terrain/resolve.js";
27
+ const BOARD = { width: 60, height: 44 };
28
+ /** Twin pairing tolerance on the point-reflected centroid (matches the editor). */
29
+ const POS_TOL = 0.75;
30
+ /** A piece this close to the board centre is self-symmetric and has no twin. */
31
+ const CENTRE_TOL = 0.3;
32
+ /** How close (inches) the reflected vertex must land to a twin vertex. */
33
+ const VERT_EPS = 0.25;
34
+ const REPO_ROOT = join(new URL("../..", import.meta.url).pathname);
35
+ const LAYOUTS_PATH = join(REPO_ROOT, "data", "core", "terrain-layouts.json");
36
+ const TEMPLATES_PATH = join(REPO_ROOT, "data", "core", "terrain-templates.json");
37
+ // ---- geometry --------------------------------------------------------------
38
+ const flipEdge = (e) => e === "left" ? "right" : e === "right" ? "left" : e === "top" ? "bottom" : "top";
39
+ const flipSide = (s) => s === "min-x" ? "max-x" : s === "max-x" ? "min-x" : s === "min-y" ? "max-y" : "min-y";
40
+ const sameRef = (a, b) => a.kind === "vertex" && b.kind === "vertex"
41
+ ? a.index === b.index
42
+ : a.kind === "face" && b.kind === "face"
43
+ ? a.side === b.side
44
+ : false;
45
+ const hasKeystone = (p, k) => (p.keystones ?? []).some((e) => e.edge === k.edge && sameRef(e.ref, k.ref));
46
+ /** Board-space vertices of an UNPARENTED piece (centroid + oriented offsets). */
47
+ function boardVertices(p, templates) {
48
+ const fp = p.footprint ?? (p.template ? templates.get(p.template)?.footprint : undefined);
49
+ if (!fp)
50
+ return null;
51
+ const offsets = orientedOffsets(fp, p.rotation_degrees ?? 0, p.mirror ?? "none");
52
+ return offsets.map((o) => ({ x: p.position.x + o.x, y: p.position.y + o.y }));
53
+ }
54
+ /**
55
+ * The keystone `k` mirrored from `primary` onto `twin`, or null (with a
56
+ * reason) when the vertex reflection has no close-enough twin vertex.
57
+ */
58
+ function mirrorKeystone(k, primaryVerts, twinVerts) {
59
+ const edge = flipEdge(k.edge);
60
+ if (k.ref.kind === "face")
61
+ return { keystone: { edge, ref: { kind: "face", side: flipSide(k.ref.side) } } };
62
+ const anchor = primaryVerts[k.ref.index];
63
+ if (!anchor)
64
+ return { error: `vertex index ${k.ref.index} out of range` };
65
+ const reflected = { x: BOARD.width - anchor.x, y: BOARD.height - anchor.y };
66
+ let bestIndex = -1;
67
+ let best = Infinity;
68
+ twinVerts.forEach((v, i) => {
69
+ const d = Math.hypot(v.x - reflected.x, v.y - reflected.y);
70
+ if (d < best) {
71
+ best = d;
72
+ bestIndex = i;
73
+ }
74
+ });
75
+ if (bestIndex < 0 || best > VERT_EPS) {
76
+ return { error: `reflected vertex lands ${best.toFixed(2)}″ from the nearest twin vertex (> ${VERT_EPS}″)` };
77
+ }
78
+ return { keystone: { edge, ref: { kind: "vertex", index: bestIndex } } };
79
+ }
80
+ /**
81
+ * Scan every layout for keystones whose symmetry twin lacks the mirror.
82
+ * `apply` mutates the layout objects in place (appending the mirrors);
83
+ * check mode leaves them untouched. Either way the report lists every
84
+ * addition and every piece that could not be handled.
85
+ */
86
+ export function pairKeystones(layouts, templates, apply) {
87
+ const byId = new Map(templates.map((t) => [t.id, t]));
88
+ const additions = [];
89
+ const warnings = [];
90
+ for (const layout of layouts) {
91
+ const pieces = layout.pieces ?? [];
92
+ // Twin pairing pass (unparented, off-centre, same template, reflected centroid).
93
+ const twin = new Map();
94
+ for (const p of pieces) {
95
+ if (twin.has(p) || p.parent_area_id)
96
+ continue;
97
+ const onCentre = Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&
98
+ Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;
99
+ if (onCentre)
100
+ continue;
101
+ const want = { x: BOARD.width - p.position.x, y: BOARD.height - p.position.y };
102
+ const match = pieces.find((q) => q !== p &&
103
+ !twin.has(q) &&
104
+ !q.parent_area_id &&
105
+ q.template === p.template &&
106
+ Math.hypot(q.position.x - want.x, q.position.y - want.y) <= POS_TOL);
107
+ if (match) {
108
+ twin.set(p, match);
109
+ twin.set(match, p);
110
+ }
111
+ }
112
+ for (const p of pieces) {
113
+ if (!p.keystones?.length)
114
+ continue;
115
+ const label = `${layout.id}/${p.id ?? p.name ?? "?"}`;
116
+ if (p.parent_area_id) {
117
+ warnings.push(`${label}: keystones on a parented feature — pair by hand`);
118
+ continue;
119
+ }
120
+ const t = twin.get(p);
121
+ if (!t) {
122
+ const onCentre = Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&
123
+ Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;
124
+ if (!onCentre)
125
+ warnings.push(`${label}: no symmetry twin found — pair by hand`);
126
+ continue; // centre pieces are their own mirror; nothing to do
127
+ }
128
+ const pv = boardVertices(p, byId);
129
+ const tv = boardVertices(t, byId);
130
+ if (!pv || !tv) {
131
+ warnings.push(`${label}: missing footprint on the pair — pair by hand`);
132
+ continue;
133
+ }
134
+ for (const k of p.keystones) {
135
+ const m = mirrorKeystone(k, pv, tv);
136
+ if ("error" in m) {
137
+ warnings.push(`${label} [${k.edge}/${JSON.stringify(k.ref)}]: ${m.error}`);
138
+ continue;
139
+ }
140
+ if (hasKeystone(t, m.keystone))
141
+ continue;
142
+ additions.push({
143
+ layoutId: layout.id,
144
+ pieceId: t.id ?? "?",
145
+ fromPieceId: p.id ?? "?",
146
+ keystone: m.keystone,
147
+ });
148
+ if (apply)
149
+ t.keystones = [...(t.keystones ?? []), m.keystone];
150
+ }
151
+ }
152
+ }
153
+ return { additions, warnings };
154
+ }
155
+ // ---- CLI -------------------------------------------------------------------
156
+ function main() {
157
+ const check = process.argv.includes("--check");
158
+ const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, "utf8"));
159
+ const templates = JSON.parse(readFileSync(TEMPLATES_PATH, "utf8"));
160
+ const report = pairKeystones(layouts, templates, !check);
161
+ for (const w of report.warnings)
162
+ console.warn(`warn: ${w}`);
163
+ const byLayout = new Map();
164
+ for (const a of report.additions)
165
+ byLayout.set(a.layoutId, (byLayout.get(a.layoutId) ?? 0) + 1);
166
+ for (const [id, n] of byLayout)
167
+ console.log(`${check ? "missing" : "added"}: ${id} — ${n} keystone(s)`);
168
+ console.log(`${report.additions.length} mirrored keystone(s) ${check ? "missing" : "added"} across ${byLayout.size} layout(s); ${report.warnings.length} warning(s)`);
169
+ if (check) {
170
+ if (report.additions.length > 0)
171
+ process.exit(1);
172
+ return;
173
+ }
174
+ if (report.additions.length > 0) {
175
+ writeFileSync(LAYOUTS_PATH, JSON.stringify(layouts, null, 2) + "\n");
176
+ console.log(`wrote ${LAYOUTS_PATH}`);
177
+ }
178
+ }
179
+ // Direct-invocation entry point (`npx tsx src/migrate-keystone-twins.ts [--check]`).
180
+ const isMain = process.argv[1] &&
181
+ resolve(process.argv[1]).replace(/\.\w+$/, "") === fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
182
+ if (isMain)
183
+ main();
184
+ //# sourceMappingURL=migrate-keystone-twins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-keystone-twins.js","sourceRoot":"","sources":["../src/migrate-keystone-twins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EACL,eAAe,GAQhB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAW,CAAC;AACjD,mFAAmF;AACnF,MAAM,OAAO,GAAG,IAAI,CAAC;AACrB,gFAAgF;AAChF,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,0EAA0E;AAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC;AAEtB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;AACnE,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAC7E,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAEjF,+EAA+E;AAE/E,MAAM,QAAQ,GAAG,CAAC,CAAY,EAAa,EAAE,CAC3C,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;AACnF,MAAM,QAAQ,GAAG,CAAC,CAAwC,EAAyC,EAAE,CACnG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAExF,MAAM,OAAO,GAAG,CAAC,CAAkB,EAAE,CAAkB,EAAW,EAAE,CAClE,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;IACxC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;IACrB,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;QACtC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QACnB,CAAC,CAAC,KAAK,CAAC;AACd,MAAM,WAAW,GAAG,CAAC,CAAc,EAAE,CAAW,EAAW,EAAE,CAC3D,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAE9E,iFAAiF;AACjF,SAAS,aAAa,CAAC,CAAc,EAAE,SAAuC;IAC5E,MAAM,EAAE,GAA0B,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACjH,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,MAAM,OAAO,GAAG,eAAe,CAAC,EAAE,EAAE,CAAC,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC;IACjF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CACrB,CAAW,EACX,YAAoB,EACpB,SAAiB;IAEjB,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC;IAC5G,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,GAAG,CAAC,KAAK,eAAe,EAAE,CAAC;IAC1E,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC;IAC5E,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;IACnB,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;YACb,IAAI,GAAG,CAAC,CAAC;YACT,SAAS,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC,CAAC,CAAC;IACH,IAAI,SAAS,GAAG,CAAC,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;QACrC,OAAO,EAAE,KAAK,EAAE,0BAA0B,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,qCAAqC,QAAQ,IAAI,EAAE,CAAC;IAC/G,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AAC3E,CAAC;AAoBD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAwB,EACxB,SAA4B,EAC5B,KAAc;IAEd,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,SAAS,GAAsB,EAAE,CAAC;IACxC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;QACnC,iFAAiF;QACjF,MAAM,IAAI,GAAG,IAAI,GAAG,EAA4B,CAAC;QACjD,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,cAAc;gBAAE,SAAS;YAC9C,MAAM,QAAQ,GACZ,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,UAAU;gBACrD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC;YACzD,IAAI,QAAQ;gBAAE,SAAS;YACvB,MAAM,IAAI,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CACvB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,KAAK,CAAC;gBACP,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBACZ,CAAC,CAAC,CAAC,cAAc;gBACjB,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;gBACzB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CACtE,CAAC;YACF,IAAI,KAAK,EAAE,CAAC;gBACV,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;gBACnB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,MAAM;gBAAE,SAAS;YACnC,MAAM,KAAK,GAAG,GAAG,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;YACtD,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;gBACrB,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,kDAAkD,CAAC,CAAC;gBAC1E,SAAS;YACX,CAAC;YACD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACtB,IAAI,CAAC,CAAC,EAAE,CAAC;gBACP,MAAM,QAAQ,GACZ,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,UAAU;oBACrD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC;gBACzD,IAAI,CAAC,QAAQ;oBAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,yCAAyC,CAAC,CAAC;gBAChF,SAAS,CAAC,oDAAoD;YAChE,CAAC;YACD,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAClC,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,gDAAgD,CAAC,CAAC;gBACxE,SAAS;YACX,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;gBAC5B,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;gBACpC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;oBACjB,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;oBAC3E,SAAS;gBACX,CAAC;gBACD,IAAI,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC;oBAAE,SAAS;gBACzC,SAAS,CAAC,IAAI,CAAC;oBACb,QAAQ,EAAE,MAAM,CAAC,EAAE;oBACnB,OAAO,EAAE,CAAC,CAAC,EAAE,IAAI,GAAG;oBACpB,WAAW,EAAE,CAAC,CAAC,EAAE,IAAI,GAAG;oBACxB,QAAQ,EAAE,CAAC,CAAC,QAAQ;iBACrB,CAAC,CAAC;gBACH,IAAI,KAAK;oBAAE,CAAC,CAAC,SAAS,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;AACjC,CAAC;AAED,+EAA+E;AAE/E,SAAS,IAAI;IACX,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAoB,CAAC;IAClF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAsB,CAAC;IAExF,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC;IAEzD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ;QAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS;QAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChG,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,QAAQ;QAAE,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;IACxG,OAAO,CAAC,GAAG,CACT,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,yBAAyB,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,WAAW,QAAQ,CAAC,IAAI,eAAe,MAAM,CAAC,QAAQ,CAAC,MAAM,aAAa,CACzJ,CAAC;IAEF,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjD,OAAO;IACT,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QACrE,OAAO,CAAC,GAAG,CAAC,SAAS,YAAY,EAAE,CAAC,CAAC;IACvC,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACf,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC1G,IAAI,MAAM;IAAE,IAAI,EAAE,CAAC","sourcesContent":["/**\n * Keystone twin pairing for `data/core/terrain-layouts.json`: every authored\n * measurement keystone should have its point-reflected counterpart on the\n * piece's 180°-symmetry twin, so a layout card prints dimension lines for\n * BOTH players' halves (the layout editor now maintains this invariant on\n * add/remove; this script back-fills the existing data and re-audits it).\n *\n * Twins are not stored — they are recovered the way the editor's\n * `autoPairTwins` does: same template, unparented, centroid within 0.75″ of\n * the point reflection through the board centre. The mirrored keystone flips\n * its board edge (left↔right, top↔bottom); a face ref flips its side, and a\n * vertex ref is resolved GEOMETRICALLY — reflect the source vertex through\n * the centre and take the twin's nearest vertex. Index arithmetic is not\n * safe: many layouts store the twin at the same angle (not θ+180), so vertex\n * i on one side need not be vertex i on the other. A reflection that lands\n * more than 0.25″ from every twin vertex is skipped with a warning rather\n * than guessed.\n *\n * Usage (from `tools/`):\n * npx tsx src/migrate-keystone-twins.ts # write missing mirrors\n * npx tsx src/migrate-keystone-twins.ts --check # report only; exit 1 if any are missing\n */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n orientedOffsets,\n type BoardEdge,\n type Footprint,\n type Keystone,\n type LayoutPiece,\n type TerrainLayout,\n type TerrainTemplate,\n type Vec2,\n} from \"./terrain/resolve.js\";\n\nconst BOARD = { width: 60, height: 44 } as const;\n/** Twin pairing tolerance on the point-reflected centroid (matches the editor). */\nconst POS_TOL = 0.75;\n/** A piece this close to the board centre is self-symmetric and has no twin. */\nconst CENTRE_TOL = 0.3;\n/** How close (inches) the reflected vertex must land to a twin vertex. */\nconst VERT_EPS = 0.25;\n\nconst REPO_ROOT = join(new URL(\"../..\", import.meta.url).pathname);\nconst LAYOUTS_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-layouts.json\");\nconst TEMPLATES_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-templates.json\");\n\n// ---- geometry --------------------------------------------------------------\n\nconst flipEdge = (e: BoardEdge): BoardEdge =>\n e === \"left\" ? \"right\" : e === \"right\" ? \"left\" : e === \"top\" ? \"bottom\" : \"top\";\nconst flipSide = (s: \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\"): \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\" =>\n s === \"min-x\" ? \"max-x\" : s === \"max-x\" ? \"min-x\" : s === \"min-y\" ? \"max-y\" : \"min-y\";\n\nconst sameRef = (a: Keystone[\"ref\"], b: Keystone[\"ref\"]): boolean =>\n a.kind === \"vertex\" && b.kind === \"vertex\"\n ? a.index === b.index\n : a.kind === \"face\" && b.kind === \"face\"\n ? a.side === b.side\n : false;\nconst hasKeystone = (p: LayoutPiece, k: Keystone): boolean =>\n (p.keystones ?? []).some((e) => e.edge === k.edge && sameRef(e.ref, k.ref));\n\n/** Board-space vertices of an UNPARENTED piece (centroid + oriented offsets). */\nfunction boardVertices(p: LayoutPiece, templates: Map<string, TerrainTemplate>): Vec2[] | null {\n const fp: Footprint | undefined = p.footprint ?? (p.template ? templates.get(p.template)?.footprint : undefined);\n if (!fp) return null;\n const offsets = orientedOffsets(fp, p.rotation_degrees ?? 0, p.mirror ?? \"none\");\n return offsets.map((o) => ({ x: p.position.x + o.x, y: p.position.y + o.y }));\n}\n\n/**\n * The keystone `k` mirrored from `primary` onto `twin`, or null (with a\n * reason) when the vertex reflection has no close-enough twin vertex.\n */\nfunction mirrorKeystone(\n k: Keystone,\n primaryVerts: Vec2[],\n twinVerts: Vec2[],\n): { keystone: Keystone } | { error: string } {\n const edge = flipEdge(k.edge);\n if (k.ref.kind === \"face\") return { keystone: { edge, ref: { kind: \"face\", side: flipSide(k.ref.side) } } };\n const anchor = primaryVerts[k.ref.index];\n if (!anchor) return { error: `vertex index ${k.ref.index} out of range` };\n const reflected = { x: BOARD.width - anchor.x, y: BOARD.height - anchor.y };\n let bestIndex = -1;\n let best = Infinity;\n twinVerts.forEach((v, i) => {\n const d = Math.hypot(v.x - reflected.x, v.y - reflected.y);\n if (d < best) {\n best = d;\n bestIndex = i;\n }\n });\n if (bestIndex < 0 || best > VERT_EPS) {\n return { error: `reflected vertex lands ${best.toFixed(2)}″ from the nearest twin vertex (> ${VERT_EPS}″)` };\n }\n return { keystone: { edge, ref: { kind: \"vertex\", index: bestIndex } } };\n}\n\n// ---- pairing + audit -------------------------------------------------------\n\nexport interface PairingAddition {\n layoutId: string;\n /** The piece RECEIVING the mirrored keystone. */\n pieceId: string;\n /** The piece whose keystone was mirrored. */\n fromPieceId: string;\n keystone: Keystone;\n}\n\nexport interface PairingReport {\n /** Mirrors that are missing (check mode) / were added (write mode). */\n additions: PairingAddition[];\n /** Keystone-bearing pieces we could not pair or mirror, with the reason. */\n warnings: string[];\n}\n\n/**\n * Scan every layout for keystones whose symmetry twin lacks the mirror.\n * `apply` mutates the layout objects in place (appending the mirrors);\n * check mode leaves them untouched. Either way the report lists every\n * addition and every piece that could not be handled.\n */\nexport function pairKeystones(\n layouts: TerrainLayout[],\n templates: TerrainTemplate[],\n apply: boolean,\n): PairingReport {\n const byId = new Map(templates.map((t) => [t.id, t]));\n const additions: PairingAddition[] = [];\n const warnings: string[] = [];\n\n for (const layout of layouts) {\n const pieces = layout.pieces ?? [];\n // Twin pairing pass (unparented, off-centre, same template, reflected centroid).\n const twin = new Map<LayoutPiece, LayoutPiece>();\n for (const p of pieces) {\n if (twin.has(p) || p.parent_area_id) continue;\n const onCentre =\n Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&\n Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;\n if (onCentre) continue;\n const want = { x: BOARD.width - p.position.x, y: BOARD.height - p.position.y };\n const match = pieces.find(\n (q) =>\n q !== p &&\n !twin.has(q) &&\n !q.parent_area_id &&\n q.template === p.template &&\n Math.hypot(q.position.x - want.x, q.position.y - want.y) <= POS_TOL,\n );\n if (match) {\n twin.set(p, match);\n twin.set(match, p);\n }\n }\n\n for (const p of pieces) {\n if (!p.keystones?.length) continue;\n const label = `${layout.id}/${p.id ?? p.name ?? \"?\"}`;\n if (p.parent_area_id) {\n warnings.push(`${label}: keystones on a parented feature — pair by hand`);\n continue;\n }\n const t = twin.get(p);\n if (!t) {\n const onCentre =\n Math.abs(p.position.x - BOARD.width / 2) < CENTRE_TOL &&\n Math.abs(p.position.y - BOARD.height / 2) < CENTRE_TOL;\n if (!onCentre) warnings.push(`${label}: no symmetry twin found — pair by hand`);\n continue; // centre pieces are their own mirror; nothing to do\n }\n const pv = boardVertices(p, byId);\n const tv = boardVertices(t, byId);\n if (!pv || !tv) {\n warnings.push(`${label}: missing footprint on the pair — pair by hand`);\n continue;\n }\n for (const k of p.keystones) {\n const m = mirrorKeystone(k, pv, tv);\n if (\"error\" in m) {\n warnings.push(`${label} [${k.edge}/${JSON.stringify(k.ref)}]: ${m.error}`);\n continue;\n }\n if (hasKeystone(t, m.keystone)) continue;\n additions.push({\n layoutId: layout.id,\n pieceId: t.id ?? \"?\",\n fromPieceId: p.id ?? \"?\",\n keystone: m.keystone,\n });\n if (apply) t.keystones = [...(t.keystones ?? []), m.keystone];\n }\n }\n }\n return { additions, warnings };\n}\n\n// ---- CLI -------------------------------------------------------------------\n\nfunction main(): void {\n const check = process.argv.includes(\"--check\");\n const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, \"utf8\")) as TerrainLayout[];\n const templates = JSON.parse(readFileSync(TEMPLATES_PATH, \"utf8\")) as TerrainTemplate[];\n\n const report = pairKeystones(layouts, templates, !check);\n\n for (const w of report.warnings) console.warn(`warn: ${w}`);\n const byLayout = new Map<string, number>();\n for (const a of report.additions) byLayout.set(a.layoutId, (byLayout.get(a.layoutId) ?? 0) + 1);\n for (const [id, n] of byLayout) console.log(`${check ? \"missing\" : \"added\"}: ${id} — ${n} keystone(s)`);\n console.log(\n `${report.additions.length} mirrored keystone(s) ${check ? \"missing\" : \"added\"} across ${byLayout.size} layout(s); ${report.warnings.length} warning(s)`,\n );\n\n if (check) {\n if (report.additions.length > 0) process.exit(1);\n return;\n }\n if (report.additions.length > 0) {\n writeFileSync(LAYOUTS_PATH, JSON.stringify(layouts, null, 2) + \"\\n\");\n console.log(`wrote ${LAYOUTS_PATH}`);\n }\n}\n\n// Direct-invocation entry point (`npx tsx src/migrate-keystone-twins.ts [--check]`).\nconst isMain =\n process.argv[1] &&\n resolve(process.argv[1]).replace(/\\.\\w+$/, \"\") === fileURLToPath(import.meta.url).replace(/\\.\\w+$/, \"\");\nif (isMain) main();\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAoC5C,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AA8C3B,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,QAAA,MAAM,WAAW,oJASP,CAAC;AACX,KAAK,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAgB9C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAmID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,CAAC,EACG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrG,IAAI,GACJ,SAAS,GACZ,MAAM,GAAG,IAAI,CAQf;AA8fD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CA4ChG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9E"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAqC5C,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AA8C3B,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,QAAA,MAAM,WAAW,oJASP,CAAC;AACX,KAAK,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAgB9C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAmID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,CAAC,EACG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrG,IAAI,GACJ,SAAS,GACZ,MAAM,GAAG,IAAI,CAQf;AA0kBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAgDhG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9E"}
package/dist/runner.js CHANGED
@@ -26,6 +26,7 @@ import { exportRoster } from "./export/index.js";
26
26
  import { importRoster, tryImportRoster } from "./import/import-roster.js";
27
27
  import { createValidator } from "./schema-loader.js";
28
28
  import { attributeStages, crunch } from "./cruncher/index.js";
29
+ import { compareCell, loadoutCell } from "./compare.js";
29
30
  import { describeScoringCard, describeAbility } from "./translate/index.js";
30
31
  import { awardsOf, scoreTurn, scoreCap, scoreSecondaryEvent, scorePrimaryEvent, emptyPlayerGame, addToHand, scoreSecondary, removeScore, setPrimary, playerPrimary, playerSecondary, playerTotal, wtcResult, } from "./scoring/index.js";
31
32
  import { resolveLayout, TerrainResolveError, keystoneMeasurements, TerrainKeystoneError, BOARD_INCHES, } from "./terrain/index.js";
@@ -482,6 +483,62 @@ function handleAttribution(state, args) {
482
483
  return err("CRUNCH_ERROR", { detail: e.message });
483
484
  }
484
485
  }
486
+ function handleCompare(state, args) {
487
+ if (typeof args !== "object" || args === null) {
488
+ return err("INVALID_INPUT", { detail: "compare args must be an object" });
489
+ }
490
+ const a = args;
491
+ const at = a.attacker ?? {};
492
+ if (typeof at.factionId !== "string" ||
493
+ typeof at.unitId !== "string" ||
494
+ typeof at.weaponId !== "string" ||
495
+ typeof at.profileIndex !== "number" ||
496
+ typeof a.targetProfileId !== "string" ||
497
+ typeof a.distance !== "number" ||
498
+ (a.phase !== "shooting" && a.phase !== "fight")) {
499
+ return err("INVALID_INPUT", { detail: "compare: malformed attacker/target/distance/phase" });
500
+ }
501
+ try {
502
+ const cell = compareCell(getDataset(state), {
503
+ factionId: at.factionId,
504
+ unitId: at.unitId,
505
+ weaponId: at.weaponId,
506
+ profileIndex: at.profileIndex,
507
+ targetProfileId: a.targetProfileId,
508
+ distance: a.distance,
509
+ phase: a.phase,
510
+ modelsFiring: typeof a.modelsFiring === "number" ? a.modelsFiring : 1,
511
+ });
512
+ return ok(cell);
513
+ }
514
+ catch (e) {
515
+ return err("UNKNOWN_ENTITY", { detail: e.message });
516
+ }
517
+ }
518
+ function handleLoadout(state, args) {
519
+ if (typeof args !== "object" || args === null) {
520
+ return err("INVALID_INPUT", { detail: "loadout args must be an object" });
521
+ }
522
+ const a = args;
523
+ if (!Array.isArray(a.lines) ||
524
+ typeof a.targetProfileId !== "string" ||
525
+ typeof a.distance !== "number" ||
526
+ (a.phase !== "shooting" && a.phase !== "fight")) {
527
+ return err("INVALID_INPUT", { detail: "loadout: malformed lines/target/distance/phase" });
528
+ }
529
+ try {
530
+ const cell = loadoutCell(getDataset(state), {
531
+ lines: a.lines,
532
+ targetProfileId: a.targetProfileId,
533
+ distance: a.distance,
534
+ phase: a.phase,
535
+ });
536
+ return ok(cell);
537
+ }
538
+ catch (e) {
539
+ return err("UNKNOWN_ENTITY", { detail: e.message });
540
+ }
541
+ }
485
542
  function handleTranslateScoring(state, args) {
486
543
  if (typeof args !== "object" || args === null) {
487
544
  return err("INVALID_INPUT", { detail: "translate_scoring args must be an object" });
@@ -747,6 +804,10 @@ export function dispatch(state, req) {
747
804
  return handleValidate(state, req.args);
748
805
  case "crunch":
749
806
  return handleCrunch(state, req.args);
807
+ case "compare":
808
+ return handleCompare(state, req.args);
809
+ case "loadout":
810
+ return handleLoadout(state, req.args);
750
811
  case "attribution":
751
812
  return handleAttribution(state, req.args);
752
813
  case "translate_scoring":