@alpaca-software/40kdc-data 0.2.0 → 0.3.1

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 (77) hide show
  1. package/dist/author-input.d.ts +20 -1
  2. package/dist/author-input.d.ts.map +1 -1
  3. package/dist/author-input.js +64 -8
  4. package/dist/author-input.js.map +1 -1
  5. package/dist/author-seed.d.ts +62 -0
  6. package/dist/author-seed.d.ts.map +1 -0
  7. package/dist/author-seed.js +194 -0
  8. package/dist/author-seed.js.map +1 -0
  9. package/dist/codegen-data.js +2 -0
  10. package/dist/codegen-data.js.map +1 -1
  11. package/dist/commands/translate.d.ts.map +1 -1
  12. package/dist/commands/translate.js +6 -68
  13. package/dist/commands/translate.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 +16 -1
  17. package/dist/data/dataset.d.ts.map +1 -1
  18. package/dist/data/dataset.js +25 -0
  19. package/dist/data/dataset.js.map +1 -1
  20. package/dist/data/index.d.ts +4 -0
  21. package/dist/data/index.d.ts.map +1 -1
  22. package/dist/data/index.js +4 -0
  23. package/dist/data/index.js.map +1 -1
  24. package/dist/data/types.d.ts +5 -1
  25. package/dist/data/types.d.ts.map +1 -1
  26. package/dist/data/types.js +2 -0
  27. package/dist/data/types.js.map +1 -1
  28. package/dist/gen-conformance.js +180 -1
  29. package/dist/gen-conformance.js.map +1 -1
  30. package/dist/generated.d.ts +309 -154
  31. package/dist/generated.d.ts.map +1 -1
  32. package/dist/generated.js.map +1 -1
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +7 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/migrate-terrain.d.ts +2 -0
  38. package/dist/migrate-terrain.d.ts.map +1 -0
  39. package/dist/migrate-terrain.js +297 -0
  40. package/dist/migrate-terrain.js.map +1 -0
  41. package/dist/runner.d.ts.map +1 -1
  42. package/dist/runner.js +42 -0
  43. package/dist/runner.js.map +1 -1
  44. package/dist/terrain/index.d.ts +11 -0
  45. package/dist/terrain/index.d.ts.map +1 -0
  46. package/dist/terrain/index.js +9 -0
  47. package/dist/terrain/index.js.map +1 -0
  48. package/dist/terrain/resolve.d.ts +122 -0
  49. package/dist/terrain/resolve.d.ts.map +1 -0
  50. package/dist/terrain/resolve.js +221 -0
  51. package/dist/terrain/resolve.js.map +1 -0
  52. package/dist/terrain/solve.d.ts +56 -0
  53. package/dist/terrain/solve.d.ts.map +1 -0
  54. package/dist/terrain/solve.js +80 -0
  55. package/dist/terrain/solve.js.map +1 -0
  56. package/dist/translate/condition.d.ts +26 -0
  57. package/dist/translate/condition.d.ts.map +1 -0
  58. package/dist/translate/condition.js +171 -0
  59. package/dist/translate/condition.js.map +1 -0
  60. package/dist/translate/index.d.ts +9 -0
  61. package/dist/translate/index.d.ts.map +1 -0
  62. package/dist/translate/index.js +9 -0
  63. package/dist/translate/index.js.map +1 -0
  64. package/dist/translate/scoring.d.ts +38 -0
  65. package/dist/translate/scoring.d.ts.map +1 -0
  66. package/dist/translate/scoring.js +80 -0
  67. package/dist/translate/scoring.js.map +1 -0
  68. package/dist/validate.d.ts.map +1 -1
  69. package/dist/validate.js +1 -0
  70. package/dist/validate.js.map +1 -1
  71. package/package.json +3 -1
  72. package/schemas/$defs/common.schema.json +43 -0
  73. package/schemas/core/secondary-card.schema.json +50 -28
  74. package/schemas/core/terrain-layout.schema.json +42 -56
  75. package/schemas/core/terrain-template.schema.json +105 -0
  76. package/schemas/enrichment/ability-dsl/condition.schema.json +5 -2
  77. package/schemas/enrichment/ability-dsl/effect.schema.json +2 -1
@@ -0,0 +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"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Card-measurement centroid solver — the inverse of the resolver's placement.
3
+ *
4
+ * Reference cards locate a terrain area by dimension lines: "this feature of the
5
+ * area is D inches from a board edge". The feature referenced varies per card
6
+ * and per piece, which is exactly why a single canonical anchor (the centroid)
7
+ * is hard to read off a card directly. This solver lets a user transcribe the
8
+ * card verbatim — pick the template, set the orientation shown, then enter one
9
+ * horizontal and one vertical dimension line against whatever feature the card
10
+ * happens to draw — and back-solves the centroid `position` the schema stores.
11
+ *
12
+ * Because the centroid is rotation- and mirror-invariant, orientation is fixed
13
+ * first; each dimension line then pins one axis of the centroid in closed form.
14
+ */
15
+ import { type Footprint, type Mirror, type Vec2 } from "./resolve.js";
16
+ /** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */
17
+ export type BoardEdge = "left" | "right" | "top" | "bottom";
18
+ /**
19
+ * Which feature of the placed area a dimension line reaches: a specific
20
+ * footprint vertex (by index, in {@link footprintVertices} order), or one of
21
+ * the placed area's axis-aligned bounding faces ("the left face", etc.).
22
+ */
23
+ export type FeatureRef = {
24
+ kind: "vertex";
25
+ index: number;
26
+ } | {
27
+ kind: "face";
28
+ side: "min-x" | "max-x" | "min-y" | "max-y";
29
+ };
30
+ /** One card dimension line: `distance` inches from `edge` to `feature`. */
31
+ export interface DimensionLine {
32
+ edge: BoardEdge;
33
+ distance: number;
34
+ feature: FeatureRef;
35
+ }
36
+ export interface SolveInput {
37
+ footprint: Footprint;
38
+ rotation: number;
39
+ mirror: Mirror;
40
+ /** Board extents in inches (40kdc standard is 60 × 44). */
41
+ board: {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ /** Two perpendicular dimension lines: exactly one must pin x, one must pin y. */
46
+ lines: [DimensionLine, DimensionLine];
47
+ }
48
+ export declare class TerrainSolveError extends Error {
49
+ }
50
+ /**
51
+ * Back-solve the centroid `position` from a template, its orientation, and two
52
+ * perpendicular card dimension lines. Closed form — one x-line and one y-line
53
+ * pin the two unknowns directly.
54
+ */
55
+ export declare function solveCentroid(input: SolveInput): Vec2;
56
+ //# sourceMappingURL=solve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAEvF,0FAA0F;AAC1F,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAA;CAAE,CAAC;AAElE,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iFAAiF;IACjF,KAAK,EAAE,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;CACvC;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAkD/C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAYrD"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Card-measurement centroid solver — the inverse of the resolver's placement.
3
+ *
4
+ * Reference cards locate a terrain area by dimension lines: "this feature of the
5
+ * area is D inches from a board edge". The feature referenced varies per card
6
+ * and per piece, which is exactly why a single canonical anchor (the centroid)
7
+ * is hard to read off a card directly. This solver lets a user transcribe the
8
+ * card verbatim — pick the template, set the orientation shown, then enter one
9
+ * horizontal and one vertical dimension line against whatever feature the card
10
+ * happens to draw — and back-solves the centroid `position` the schema stores.
11
+ *
12
+ * Because the centroid is rotation- and mirror-invariant, orientation is fixed
13
+ * first; each dimension line then pins one axis of the centroid in closed form.
14
+ */
15
+ import { orientedOffsets } from "./resolve.js";
16
+ export class TerrainSolveError extends Error {
17
+ }
18
+ /** The signed offset (from the centroid) the given feature resolves to, on its axis. */
19
+ function featureOffset(offsets, feature, axis) {
20
+ if (feature.kind === "vertex") {
21
+ const o = offsets[feature.index];
22
+ if (!o)
23
+ throw new TerrainSolveError(`vertex index ${feature.index} out of range`);
24
+ return axis === "x" ? o.x : o.y;
25
+ }
26
+ const xs = offsets.map((o) => o.x);
27
+ const ys = offsets.map((o) => o.y);
28
+ switch (feature.side) {
29
+ case "min-x":
30
+ return Math.min(...xs);
31
+ case "max-x":
32
+ return Math.max(...xs);
33
+ case "min-y":
34
+ return Math.min(...ys);
35
+ case "max-y":
36
+ return Math.max(...ys);
37
+ }
38
+ }
39
+ function axisOfEdge(edge) {
40
+ return edge === "left" || edge === "right" ? "x" : "y";
41
+ }
42
+ /** Solve one axis of the centroid from a single dimension line. */
43
+ function solveAxis(line, offsets, board) {
44
+ const axis = axisOfEdge(line.edge);
45
+ const o = featureOffset(offsets, line.feature, axis);
46
+ // edge → centroid: near-side edges measure from 0; far-side from the extent.
47
+ let value;
48
+ switch (line.edge) {
49
+ case "left":
50
+ value = line.distance - o;
51
+ break;
52
+ case "right":
53
+ value = board.width - line.distance - o;
54
+ break;
55
+ case "top":
56
+ value = line.distance - o;
57
+ break;
58
+ case "bottom":
59
+ value = board.height - line.distance - o;
60
+ break;
61
+ }
62
+ return { axis, value };
63
+ }
64
+ /**
65
+ * Back-solve the centroid `position` from a template, its orientation, and two
66
+ * perpendicular card dimension lines. Closed form — one x-line and one y-line
67
+ * pin the two unknowns directly.
68
+ */
69
+ export function solveCentroid(input) {
70
+ const offsets = orientedOffsets(input.footprint, input.rotation, input.mirror);
71
+ const a = solveAxis(input.lines[0], offsets, input.board);
72
+ const b = solveAxis(input.lines[1], offsets, input.board);
73
+ if (a.axis === b.axis) {
74
+ throw new TerrainSolveError("the two dimension lines must pin different axes (one of left/right, one of top/bottom)");
75
+ }
76
+ const x = a.axis === "x" ? a.value : b.value;
77
+ const y = a.axis === "y" ? a.value : b.value;
78
+ return { x, y };
79
+ }
80
+ //# sourceMappingURL=solve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solve.js","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Card-measurement centroid solver — the inverse of the resolver's placement.\n *\n * Reference cards locate a terrain area by dimension lines: \"this feature of the\n * area is D inches from a board edge\". The feature referenced varies per card\n * and per piece, which is exactly why a single canonical anchor (the centroid)\n * is hard to read off a card directly. This solver lets a user transcribe the\n * card verbatim — pick the template, set the orientation shown, then enter one\n * horizontal and one vertical dimension line against whatever feature the card\n * happens to draw — and back-solves the centroid `position` the schema stores.\n *\n * Because the centroid is rotation- and mirror-invariant, orientation is fixed\n * first; each dimension line then pins one axis of the centroid in closed form.\n */\nimport { orientedOffsets, type Footprint, type Mirror, type Vec2 } from \"./resolve.js\";\n\n/** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */\nexport type BoardEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/**\n * Which feature of the placed area a dimension line reaches: a specific\n * footprint vertex (by index, in {@link footprintVertices} order), or one of\n * the placed area's axis-aligned bounding faces (\"the left face\", etc.).\n */\nexport type FeatureRef =\n | { kind: \"vertex\"; index: number }\n | { kind: \"face\"; side: \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\" };\n\n/** One card dimension line: `distance` inches from `edge` to `feature`. */\nexport interface DimensionLine {\n edge: BoardEdge;\n distance: number;\n feature: FeatureRef;\n}\n\nexport interface SolveInput {\n footprint: Footprint;\n rotation: number;\n mirror: Mirror;\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n /** Two perpendicular dimension lines: exactly one must pin x, one must pin y. */\n lines: [DimensionLine, DimensionLine];\n}\n\nexport class TerrainSolveError extends Error {}\n\n/** The signed offset (from the centroid) the given feature resolves to, on its axis. */\nfunction featureOffset(offsets: Vec2[], feature: FeatureRef, axis: \"x\" | \"y\"): number {\n if (feature.kind === \"vertex\") {\n const o = offsets[feature.index];\n if (!o) throw new TerrainSolveError(`vertex index ${feature.index} out of range`);\n return axis === \"x\" ? o.x : o.y;\n }\n const xs = offsets.map((o) => o.x);\n const ys = offsets.map((o) => o.y);\n switch (feature.side) {\n case \"min-x\":\n return Math.min(...xs);\n case \"max-x\":\n return Math.max(...xs);\n case \"min-y\":\n return Math.min(...ys);\n case \"max-y\":\n return Math.max(...ys);\n }\n}\n\nfunction axisOfEdge(edge: BoardEdge): \"x\" | \"y\" {\n return edge === \"left\" || edge === \"right\" ? \"x\" : \"y\";\n}\n\n/** Solve one axis of the centroid from a single dimension line. */\nfunction solveAxis(line: DimensionLine, offsets: Vec2[], board: { width: number; height: number }): { axis: \"x\" | \"y\"; value: number } {\n const axis = axisOfEdge(line.edge);\n const o = featureOffset(offsets, line.feature, axis);\n // edge → centroid: near-side edges measure from 0; far-side from the extent.\n let value: number;\n switch (line.edge) {\n case \"left\":\n value = line.distance - o;\n break;\n case \"right\":\n value = board.width - line.distance - o;\n break;\n case \"top\":\n value = line.distance - o;\n break;\n case \"bottom\":\n value = board.height - line.distance - o;\n break;\n }\n return { axis, value };\n}\n\n/**\n * Back-solve the centroid `position` from a template, its orientation, and two\n * perpendicular card dimension lines. Closed form — one x-line and one y-line\n * pin the two unknowns directly.\n */\nexport function solveCentroid(input: SolveInput): Vec2 {\n const offsets = orientedOffsets(input.footprint, input.rotation, input.mirror);\n const a = solveAxis(input.lines[0], offsets, input.board);\n const b = solveAxis(input.lines[1], offsets, input.board);\n if (a.axis === b.axis) {\n throw new TerrainSolveError(\n \"the two dimension lines must pin different axes (one of left/right, one of top/bottom)\",\n );\n }\n const x = a.axis === \"x\" ? a.value : b.value;\n const y = a.axis === \"y\" ? a.value : b.value;\n return { x, y };\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Humanize an Ability-DSL / scoring `condition` into plain English.
3
+ *
4
+ * Shared by the ability-text CLI (`commands/translate.ts`) and the scoring-card
5
+ * translator (`scoring.ts`). Output is **ASCII-only** with a fixed clause and
6
+ * parameter order: it is pinned byte-for-byte across the TS and Rust ports by
7
+ * the `conformance/scoring-translation` corpus, so any phrasing change here is a
8
+ * semantic corpus change (bump `conformance/SPEC_VERSION`).
9
+ */
10
+ /**
11
+ * Minimal structural view of a condition node. Matches both the ability-dsl
12
+ * condition schema and the `secondary-card` award `when` field (a simple node
13
+ * carries `type` + `parameters` + `negated`; a compound node carries
14
+ * `operator` + `operands`).
15
+ */
16
+ export interface Condition {
17
+ type?: string;
18
+ operator?: "and" | "or" | "not";
19
+ operands?: Condition[];
20
+ parameters?: Record<string, unknown>;
21
+ negated?: boolean;
22
+ }
23
+ /** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */
24
+ export declare function dekebab(s: string): string;
25
+ export declare function describeCondition(c: Condition): string;
26
+ //# sourceMappingURL=condition.d.ts.map
@@ -0,0 +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,CAoItD"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Humanize an Ability-DSL / scoring `condition` into plain English.
3
+ *
4
+ * Shared by the ability-text CLI (`commands/translate.ts`) and the scoring-card
5
+ * translator (`scoring.ts`). Output is **ASCII-only** with a fixed clause and
6
+ * parameter order: it is pinned byte-for-byte across the TS and Rust ports by
7
+ * the `conformance/scoring-translation` corpus, so any phrasing change here is a
8
+ * semantic corpus change (bump `conformance/SPEC_VERSION`).
9
+ */
10
+ /** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */
11
+ export function dekebab(s) {
12
+ return s.replace(/-/g, " ");
13
+ }
14
+ function str(v) {
15
+ return typeof v === "string" ? v : String(v);
16
+ }
17
+ /** `2` + `objective` → `2+ objectives`. Nouns here are all regular plurals. */
18
+ function count(n, noun) {
19
+ return `${str(n)}+ ${noun}s`;
20
+ }
21
+ export function describeCondition(c) {
22
+ // Compound nodes first — join the operands with lowercase connectives so the
23
+ // result reads naturally inside a "... when X and Y" clause.
24
+ if (c.operator === "and" && c.operands) {
25
+ return c.operands.map(describeCondition).join(" and ");
26
+ }
27
+ if (c.operator === "or" && c.operands) {
28
+ return c.operands.map(describeCondition).join(" or ");
29
+ }
30
+ if (c.operator === "not" && c.operands) {
31
+ return `not (${c.operands.map(describeCondition).join(", ")})`;
32
+ }
33
+ const negate = c.negated ? "not " : "";
34
+ const p = c.parameters ?? {};
35
+ switch (c.type) {
36
+ // ── Ability-DSL conditions (ported from commands/translate.ts) ──────────
37
+ case "phase-is":
38
+ return `${negate}during the ${str(p.phase)} phase`;
39
+ case "timing-is":
40
+ return `${negate}at ${dekebab(str(p.timing))}`;
41
+ case "player-turn-is":
42
+ return `${negate}in ${p.turn === "your-turn" ? "your" : p.turn === "opponent-turn" ? "the opponent's" : "either player's"} turn`;
43
+ case "charged-this-turn":
44
+ return `${negate}the unit charged this turn`;
45
+ case "advanced-this-turn":
46
+ return `${negate}the unit advanced this turn`;
47
+ case "remained-stationary":
48
+ return `${negate}the unit remained stationary`;
49
+ case "unit-below-starting-strength":
50
+ return `${negate}the unit is below starting strength`;
51
+ case "unit-below-half-strength":
52
+ return `${negate}the unit is below half strength`;
53
+ case "unit-has-keyword":
54
+ return `${negate}the unit has "${str(p.keyword)}"`;
55
+ case "target-has-keyword":
56
+ return `${negate}the target has "${str(p.keyword)}"`;
57
+ case "model-is-leader":
58
+ return `${negate}the model is leading a unit`;
59
+ case "is-attached":
60
+ return `${negate}attached to a ${p.keyword ? `${str(p.keyword)} ` : ""}unit`;
61
+ case "attack-is-type":
62
+ return `${negate}for ${str(p.attack_type)} attacks`;
63
+ case "is-battle-shocked":
64
+ return `${negate}the unit is battle-shocked`;
65
+ case "has-lost-wounds":
66
+ return `${negate}the model has lost wounds`;
67
+ case "opponent-unit-within-range":
68
+ return `${negate}an enemy unit is within ${p.range === "engagement" ? "engagement range" : `${str(p.range)}"`}`;
69
+ case "unit-within-range-of":
70
+ return `${negate}within ${str(p.range)}" of ${str(p.target_type ?? "target")}${p.keyword ? ` (${str(p.keyword)})` : ""}`;
71
+ case "within-range-of-objective":
72
+ return `${negate}within range of an objective`;
73
+ case "has-fought-this-phase":
74
+ return `${negate}has fought this phase`;
75
+ case "destroyed-by-attack-type":
76
+ return `${negate}destroyed by a ${str(p.attack_type)} attack`;
77
+ // ── Scoring conditions (secondary-card award `when`) ────────────────────
78
+ case "objective-majority":
79
+ return `${negate}you hold more objectives than the ${dekebab(str(p.relative_to ?? "opponent"))}`;
80
+ case "controls-objective": {
81
+ const noun = p.objective_role ? `${dekebab(str(p.objective_role))} objective` : "objective";
82
+ let s = `${negate}you control ${count(p.count_min ?? 1, noun)}`;
83
+ if (p.objective != null)
84
+ s += ` (${dekebab(str(p.objective))})`;
85
+ if (p.scope != null)
86
+ s += ` in ${dekebab(str(p.scope))}`;
87
+ if (p.exclude != null)
88
+ s += ` (excluding ${dekebab(str(p.exclude))})`;
89
+ return s;
90
+ }
91
+ case "units-destroyed":
92
+ return `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed ${dekebab(str(p.window))}`;
93
+ case "units-destroyed-comparison": {
94
+ const subj = (p.subject ?? {});
95
+ const ref = (p.reference ?? {});
96
+ const cmp = p.comparator === "greater-or-equal" ? "at least as many" : "more";
97
+ const link = p.comparator === "greater-or-equal" ? "as" : "than";
98
+ return `${negate}you destroyed ${cmp} ${str(subj.side)} units ${dekebab(str(subj.window))} ${link} ${str(ref.side)} units ${dekebab(str(ref.window))}`;
99
+ }
100
+ case "new-objective-controlled":
101
+ return `${negate}you newly control ${count(p.count_min ?? 1, "objective")} this turn`;
102
+ case "destroyed-while-on-objective": {
103
+ let s = `${negate}${count(p.count_min ?? 1, "enemy unit")} destroyed`;
104
+ if (p.destroyer_on_objective)
105
+ s += " by a unit on an objective";
106
+ if (p.victim_on_objective)
107
+ s += " while on an objective";
108
+ return s;
109
+ }
110
+ case "action-completed": {
111
+ let s = `${negate}${count(p.count_min ?? 1, "action")} completed`;
112
+ if (p.action_id != null)
113
+ s += ` (${dekebab(str(p.action_id))})`;
114
+ if (p.target_kind != null)
115
+ s += ` on ${dekebab(str(p.target_kind))}`;
116
+ const tf = (p.target_filter ?? {});
117
+ if (tf.objective_role != null)
118
+ s += ` (${dekebab(str(tf.objective_role))})`;
119
+ if (tf.in_enemy_territory)
120
+ s += " in enemy territory";
121
+ if (tf.exclude != null)
122
+ s += ` (excluding ${dekebab(str(tf.exclude))})`;
123
+ if (p.window != null)
124
+ s += ` ${dekebab(str(p.window))}`;
125
+ return s;
126
+ }
127
+ case "objective-has-tag": {
128
+ let s = `${negate}${count(p.count_min ?? 1, "objective")} tagged ${dekebab(str(p.tag))}`;
129
+ if (p.count_max != null)
130
+ s += ` (at most ${str(p.count_max)})`;
131
+ if (p.objective != null)
132
+ s += ` (${dekebab(str(p.objective))})`;
133
+ if (p.scope != null)
134
+ s += ` in ${dekebab(str(p.scope))}`;
135
+ if (p.last_marked)
136
+ s += " (most recently marked)";
137
+ return s;
138
+ }
139
+ case "unit-has-tag": {
140
+ let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} tagged ${dekebab(str(p.tag))}`;
141
+ if (p.window != null)
142
+ s += ` (${dekebab(str(p.window))})`;
143
+ return s;
144
+ }
145
+ case "terrain-has-tag": {
146
+ let s = `${negate}terrain tagged ${dekebab(str(p.tag))}`;
147
+ if (p.friendly_units_min != null)
148
+ s += ` with ${str(p.friendly_units_min)}+ friendly units`;
149
+ if (p.enemy_units_max != null)
150
+ s += ` and at most ${str(p.enemy_units_max)} enemy units`;
151
+ if (p.last_marked)
152
+ s += " (most recently marked)";
153
+ if (p.in_enemy_dz)
154
+ s += " in the enemy deployment zone";
155
+ return s;
156
+ }
157
+ case "terrain-area-control":
158
+ return `${negate}you control a terrain area with ${str(p.min_models ?? 1)}+ models`;
159
+ case "territory-control": {
160
+ let s = `${negate}you control ${dekebab(str(p.territory_ref ?? "your-territory"))}`;
161
+ if (p.enemy_units_max != null)
162
+ s += ` with at most ${str(p.enemy_units_max)} enemy units`;
163
+ return s;
164
+ }
165
+ case "engagement-fronts":
166
+ return `${negate}you are engaged on ${str(p.count_min ?? 1)}+ fronts`;
167
+ default:
168
+ return `${negate}${dekebab(c.type ?? "unknown")}`;
169
+ }
170
+ }
171
+ //# sourceMappingURL=condition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"condition.js","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"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","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 \"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"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Plain-English translation of structured game data — currently the
3
+ * `secondary-card` scoring `awards` (mission "how to play" readouts) and the
4
+ * shared Ability-DSL condition humanizer. Output is ASCII-only and pinned
5
+ * across language ports by the `conformance/scoring-translation` corpus.
6
+ */
7
+ export { describeCondition, dekebab, type Condition } from "./condition.js";
8
+ export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, } from "./scoring.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Plain-English translation of structured game data — currently the
3
+ * `secondary-card` scoring `awards` (mission "how to play" readouts) and the
4
+ * shared Ability-DSL condition humanizer. Output is ASCII-only and pinned
5
+ * across language ports by the `conformance/scoring-translation` corpus.
6
+ */
7
+ export { describeCondition, dekebab } from "./condition.js";
8
+ export { describeTrigger, describeAward, describeScoringCard, } from "./scoring.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAGpB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — currently the\n * `secondary-card` scoring `awards` (mission \"how to play\" readouts) and the\n * shared Ability-DSL condition humanizer. Output is ASCII-only and pinned\n * across language ports by the `conformance/scoring-translation` corpus.\n */\nexport { describeCondition, dekebab, type Condition } from \"./condition.js\";\nexport {\n describeTrigger,\n describeAward,\n describeScoringCard,\n type ScoringTrigger,\n type ScoringAward,\n} from \"./scoring.js\";\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Humanize a `secondary-card` scoring `award` into plain English.
3
+ *
4
+ * Output is **ASCII-only** with a fixed clause order, pinned byte-for-byte
5
+ * across the TS and Rust ports by the `conformance/scoring-translation` corpus.
6
+ * The community `text` summary and the `actions` list are verbatim data, not
7
+ * translation, so they are not produced here — only the structured `awards`.
8
+ */
9
+ import type { SecondaryCard } from "../generated.js";
10
+ import { type Condition } from "./condition.js";
11
+ /** When a VP award is evaluated (the `trigger` block on an award). */
12
+ export interface ScoringTrigger {
13
+ timing?: "start-of-turn" | "end-of-turn" | "start-of-phase" | "end-of-phase" | "end-of-battle";
14
+ phase?: "command" | "movement" | "shooting" | "charge" | "fight";
15
+ player_turn?: "your-turn" | "opponent-turn" | "either";
16
+ battle_round?: {
17
+ min?: number;
18
+ max?: number;
19
+ };
20
+ }
21
+ /** One VP-award block on a scoring card. */
22
+ export interface ScoringAward {
23
+ trigger?: ScoringTrigger;
24
+ when?: Condition;
25
+ vp?: number;
26
+ vp_per?: number;
27
+ per?: string;
28
+ per_max?: number;
29
+ cumulative?: boolean;
30
+ exclusive_group?: string;
31
+ }
32
+ /** "End of your Command phase (round 2+)" and friends. */
33
+ export declare function describeTrigger(t: ScoringTrigger): string;
34
+ /** "End of your Command phase (round 2+): 3 VP per controlled objective when ..." */
35
+ export declare function describeAward(a: ScoringAward): string;
36
+ /** Humanize every award on a card, in array order (the order is load-bearing). */
37
+ export declare function describeScoringCard(card: SecondaryCard): string[];
38
+ //# sourceMappingURL=scoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,GAAG,eAAe,CAAC;IAC/F,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC;IACvD,YAAY,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAyCzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAiBrD;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAGjE"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Humanize a `secondary-card` scoring `award` into plain English.
3
+ *
4
+ * Output is **ASCII-only** with a fixed clause order, pinned byte-for-byte
5
+ * across the TS and Rust ports by the `conformance/scoring-translation` corpus.
6
+ * The community `text` summary and the `actions` list are verbatim data, not
7
+ * translation, so they are not produced here — only the structured `awards`.
8
+ */
9
+ import { describeCondition, dekebab } from "./condition.js";
10
+ function capitalize(s) {
11
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
12
+ }
13
+ /** "End of your Command phase (round 2+)" and friends. */
14
+ export function describeTrigger(t) {
15
+ const turn = t.player_turn === "opponent-turn"
16
+ ? "the opponent's"
17
+ : t.player_turn === "either"
18
+ ? "any"
19
+ : "your";
20
+ let base;
21
+ switch (t.timing) {
22
+ case "start-of-turn":
23
+ base = `Start of ${turn} turn`;
24
+ break;
25
+ case "end-of-turn":
26
+ base = `End of ${turn} turn`;
27
+ break;
28
+ case "start-of-phase":
29
+ base = `Start of ${turn} ${capitalize(t.phase ?? "")} phase`;
30
+ break;
31
+ case "end-of-phase":
32
+ base = `End of ${turn} ${capitalize(t.phase ?? "")} phase`;
33
+ break;
34
+ case "end-of-battle":
35
+ base = "End of the battle";
36
+ break;
37
+ default:
38
+ base = t.phase ? `During ${turn} ${capitalize(t.phase)} phase` : "Any time";
39
+ }
40
+ const br = t.battle_round;
41
+ if (br) {
42
+ const { min, max } = br;
43
+ if (min != null && max != null) {
44
+ base += min === max ? ` (round ${min})` : ` (rounds ${min}-${max})`;
45
+ }
46
+ else if (min != null) {
47
+ base += ` (round ${min}+)`;
48
+ }
49
+ else if (max != null) {
50
+ base += ` (rounds 1-${max})`;
51
+ }
52
+ }
53
+ return base;
54
+ }
55
+ /** "End of your Command phase (round 2+): 3 VP per controlled objective when ..." */
56
+ export function describeAward(a) {
57
+ const trigger = a.trigger ? describeTrigger(a.trigger) : "Any time";
58
+ let amount;
59
+ if (a.vp != null) {
60
+ amount = `${a.vp} VP`;
61
+ }
62
+ else if (a.vp_per != null) {
63
+ amount = `${a.vp_per} VP per ${a.per ? dekebab(a.per) : "instance"}`;
64
+ if (a.per_max != null)
65
+ amount += ` (max ${a.per_max})`;
66
+ }
67
+ else {
68
+ amount = "no VP";
69
+ }
70
+ const prefix = a.cumulative ? "+ " : "";
71
+ const when = a.when ? ` when ${describeCondition(a.when)}` : "";
72
+ const tier = a.exclusive_group ? " [highest tier]" : "";
73
+ return `${prefix}${trigger}: ${amount}${when}${tier}`;
74
+ }
75
+ /** Humanize every award on a card, in array order (the order is load-bearing). */
76
+ export function describeScoringCard(card) {
77
+ const awards = (card.awards ?? []);
78
+ return awards.map(describeAward);
79
+ }
80
+ //# sourceMappingURL=scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAsB5E,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,eAAe,CAAC,CAAiB;IAC/C,MAAM,IAAI,GACR,CAAC,CAAC,WAAW,KAAK,eAAe;QAC/B,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,CAAC,CAAC,WAAW,KAAK,QAAQ;YAC1B,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,MAAM,CAAC;IAEf,IAAI,IAAY,CAAC;IACjB,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,eAAe;YAClB,IAAI,GAAG,YAAY,IAAI,OAAO,CAAC;YAC/B,MAAM;QACR,KAAK,aAAa;YAChB,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC;YAC7B,MAAM;QACR,KAAK,gBAAgB;YACnB,IAAI,GAAG,YAAY,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC;YAC7D,MAAM;QACR,KAAK,cAAc;YACjB,IAAI,GAAG,UAAU,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC;YAC3D,MAAM;QACR,KAAK,eAAe;YAClB,IAAI,GAAG,mBAAmB,CAAC;YAC3B,MAAM;QACR;YACE,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;IAChF,CAAC;IAED,MAAM,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC;IAC1B,IAAI,EAAE,EAAE,CAAC;QACP,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YAC/B,IAAI,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,YAAY,GAAG,IAAI,GAAG,GAAG,CAAC;QACtE,CAAC;aAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,IAAI,WAAW,GAAG,IAAI,CAAC;QAC7B,CAAC;aAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,IAAI,cAAc,GAAG,GAAG,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,aAAa,CAAC,CAAe;IAC3C,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IAEpE,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC;IACxB,CAAC;SAAM,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;QACrE,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI;YAAE,MAAM,IAAI,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,OAAO,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,OAAO,GAAG,MAAM,GAAG,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,IAAmB;IACrD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAA8B,CAAC;IAChE,OAAO,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Humanize a `secondary-card` scoring `award` into plain English.\n *\n * Output is **ASCII-only** with a fixed clause order, pinned byte-for-byte\n * across the TS and Rust ports by the `conformance/scoring-translation` corpus.\n * The community `text` summary and the `actions` list are verbatim data, not\n * translation, so they are not produced here — only the structured `awards`.\n */\n\nimport type { SecondaryCard } from \"../generated.js\";\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/** When a VP award is evaluated (the `trigger` block on an award). */\nexport interface ScoringTrigger {\n timing?: \"start-of-turn\" | \"end-of-turn\" | \"start-of-phase\" | \"end-of-phase\" | \"end-of-battle\";\n phase?: \"command\" | \"movement\" | \"shooting\" | \"charge\" | \"fight\";\n player_turn?: \"your-turn\" | \"opponent-turn\" | \"either\";\n battle_round?: { min?: number; max?: number };\n}\n\n/** One VP-award block on a scoring card. */\nexport interface ScoringAward {\n trigger?: ScoringTrigger;\n when?: Condition;\n vp?: number;\n vp_per?: number;\n per?: string;\n per_max?: number;\n cumulative?: boolean;\n exclusive_group?: string;\n}\n\nfunction capitalize(s: string): string {\n return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);\n}\n\n/** \"End of your Command phase (round 2+)\" and friends. */\nexport function describeTrigger(t: ScoringTrigger): string {\n const turn =\n t.player_turn === \"opponent-turn\"\n ? \"the opponent's\"\n : t.player_turn === \"either\"\n ? \"any\"\n : \"your\";\n\n let base: string;\n switch (t.timing) {\n case \"start-of-turn\":\n base = `Start of ${turn} turn`;\n break;\n case \"end-of-turn\":\n base = `End of ${turn} turn`;\n break;\n case \"start-of-phase\":\n base = `Start of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-phase\":\n base = `End of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-battle\":\n base = \"End of the battle\";\n break;\n default:\n base = t.phase ? `During ${turn} ${capitalize(t.phase)} phase` : \"Any time\";\n }\n\n const br = t.battle_round;\n if (br) {\n const { min, max } = br;\n if (min != null && max != null) {\n base += min === max ? ` (round ${min})` : ` (rounds ${min}-${max})`;\n } else if (min != null) {\n base += ` (round ${min}+)`;\n } else if (max != null) {\n base += ` (rounds 1-${max})`;\n }\n }\n return base;\n}\n\n/** \"End of your Command phase (round 2+): 3 VP per controlled objective when ...\" */\nexport function describeAward(a: ScoringAward): string {\n const trigger = a.trigger ? describeTrigger(a.trigger) : \"Any time\";\n\n let amount: string;\n if (a.vp != null) {\n amount = `${a.vp} VP`;\n } else if (a.vp_per != null) {\n amount = `${a.vp_per} VP per ${a.per ? dekebab(a.per) : \"instance\"}`;\n if (a.per_max != null) amount += ` (max ${a.per_max})`;\n } else {\n amount = \"no VP\";\n }\n\n const prefix = a.cumulative ? \"+ \" : \"\";\n const when = a.when ? ` when ${describeCondition(a.when)}` : \"\";\n const tier = a.exclusive_group ? \" [highest tier]\" : \"\";\n return `${prefix}${trigger}: ${amount}${when}${tier}`;\n}\n\n/** Humanize every award on a card, in array order (the order is load-bearing). */\nexport function describeScoringCard(card: SecondaryCard): string[] {\n const awards = (card.awards ?? []) as unknown as ScoringAward[];\n return awards.map(describeAward);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AA4CD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CA+E3B"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AA6CD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CA+E3B"}
package/dist/validate.js CHANGED
@@ -24,6 +24,7 @@ const SCHEMA_MAP = {
24
24
  "mission-matchups": "https://40kdc.dev/schemas/core/mission-matchup.schema.json",
25
25
  missions: "https://40kdc.dev/schemas/core/mission.schema.json",
26
26
  "secondary-cards": "https://40kdc.dev/schemas/core/secondary-card.schema.json",
27
+ "terrain-templates": "https://40kdc.dev/schemas/core/terrain-template.schema.json",
27
28
  "terrain-layouts": "https://40kdc.dev/schemas/core/terrain-layout.schema.json",
28
29
  "phase-mappings": "https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json",
29
30
  "timing-flags": "https://40kdc.dev/schemas/enrichment/timing-flag.schema.json",