@thijshakkenberg/valkyrie-mom-mcp 1.0.0

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 (180) hide show
  1. package/.claude-plugin/plugin.json +17 -0
  2. package/.mcp.json +9 -0
  3. package/CLAUDE.md +73 -0
  4. package/LICENSE +21 -0
  5. package/README.md +164 -0
  6. package/agents/scenario-designer.md +114 -0
  7. package/dist/catalogs/catalog-store.d.ts +20 -0
  8. package/dist/catalogs/catalog-store.d.ts.map +1 -0
  9. package/dist/catalogs/catalog-store.js +78 -0
  10. package/dist/catalogs/catalog-store.js.map +1 -0
  11. package/dist/catalogs/catalog-types.d.ts +10 -0
  12. package/dist/catalogs/catalog-types.d.ts.map +1 -0
  13. package/dist/catalogs/catalog-types.js +2 -0
  14. package/dist/catalogs/catalog-types.js.map +1 -0
  15. package/dist/catalogs/data/all-catalogs.d.ts +9 -0
  16. package/dist/catalogs/data/all-catalogs.d.ts.map +1 -0
  17. package/dist/catalogs/data/all-catalogs.js +8383 -0
  18. package/dist/catalogs/data/all-catalogs.js.map +1 -0
  19. package/dist/catalogs/data/tile-geometry.d.ts +12 -0
  20. package/dist/catalogs/data/tile-geometry.d.ts.map +1 -0
  21. package/dist/catalogs/data/tile-geometry.js +183 -0
  22. package/dist/catalogs/data/tile-geometry.js.map +1 -0
  23. package/dist/catalogs/extract-catalogs.d.ts +12 -0
  24. package/dist/catalogs/extract-catalogs.d.ts.map +1 -0
  25. package/dist/catalogs/extract-catalogs.js +128 -0
  26. package/dist/catalogs/extract-catalogs.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +13 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/io/ini-parser.d.ts +6 -0
  32. package/dist/io/ini-parser.d.ts.map +1 -0
  33. package/dist/io/ini-parser.js +42 -0
  34. package/dist/io/ini-parser.js.map +1 -0
  35. package/dist/io/ini-writer.d.ts +2 -0
  36. package/dist/io/ini-writer.d.ts.map +1 -0
  37. package/dist/io/ini-writer.js +36 -0
  38. package/dist/io/ini-writer.js.map +1 -0
  39. package/dist/io/localization-io.d.ts +21 -0
  40. package/dist/io/localization-io.d.ts.map +1 -0
  41. package/dist/io/localization-io.js +51 -0
  42. package/dist/io/localization-io.js.map +1 -0
  43. package/dist/io/package-builder.d.ts +2 -0
  44. package/dist/io/package-builder.d.ts.map +1 -0
  45. package/dist/io/package-builder.js +22 -0
  46. package/dist/io/package-builder.js.map +1 -0
  47. package/dist/model/component-types.d.ts +61 -0
  48. package/dist/model/component-types.d.ts.map +1 -0
  49. package/dist/model/component-types.js +71 -0
  50. package/dist/model/component-types.js.map +1 -0
  51. package/dist/model/localization-store.d.ts +15 -0
  52. package/dist/model/localization-store.d.ts.map +1 -0
  53. package/dist/model/localization-store.js +42 -0
  54. package/dist/model/localization-store.js.map +1 -0
  55. package/dist/model/scenario-model.d.ts +27 -0
  56. package/dist/model/scenario-model.d.ts.map +1 -0
  57. package/dist/model/scenario-model.js +151 -0
  58. package/dist/model/scenario-model.js.map +1 -0
  59. package/dist/resources/format-docs.d.ts +6 -0
  60. package/dist/resources/format-docs.d.ts.map +1 -0
  61. package/dist/resources/format-docs.js +261 -0
  62. package/dist/resources/format-docs.js.map +1 -0
  63. package/dist/server.d.ts +3 -0
  64. package/dist/server.d.ts.map +1 -0
  65. package/dist/server.js +206 -0
  66. package/dist/server.js.map +1 -0
  67. package/dist/tools/lifecycle.d.ts +37 -0
  68. package/dist/tools/lifecycle.d.ts.map +1 -0
  69. package/dist/tools/lifecycle.js +146 -0
  70. package/dist/tools/lifecycle.js.map +1 -0
  71. package/dist/tools/map.d.ts +14 -0
  72. package/dist/tools/map.d.ts.map +1 -0
  73. package/dist/tools/map.js +98 -0
  74. package/dist/tools/map.js.map +1 -0
  75. package/dist/tools/reference.d.ts +4 -0
  76. package/dist/tools/reference.d.ts.map +1 -0
  77. package/dist/tools/reference.js +6 -0
  78. package/dist/tools/reference.js.map +1 -0
  79. package/dist/tools/shared.d.ts +10 -0
  80. package/dist/tools/shared.d.ts.map +1 -0
  81. package/dist/tools/shared.js +22 -0
  82. package/dist/tools/shared.js.map +1 -0
  83. package/dist/tools/upsert.d.ts +15 -0
  84. package/dist/tools/upsert.d.ts.map +1 -0
  85. package/dist/tools/upsert.js +91 -0
  86. package/dist/tools/upsert.js.map +1 -0
  87. package/dist/validation/rules/catalog-references.d.ts +14 -0
  88. package/dist/validation/rules/catalog-references.d.ts.map +1 -0
  89. package/dist/validation/rules/catalog-references.js +90 -0
  90. package/dist/validation/rules/catalog-references.js.map +1 -0
  91. package/dist/validation/rules/cross-references.d.ts +9 -0
  92. package/dist/validation/rules/cross-references.d.ts.map +1 -0
  93. package/dist/validation/rules/cross-references.js +37 -0
  94. package/dist/validation/rules/cross-references.js.map +1 -0
  95. package/dist/validation/rules/event-chain-utils.d.ts +8 -0
  96. package/dist/validation/rules/event-chain-utils.d.ts.map +1 -0
  97. package/dist/validation/rules/event-chain-utils.js +30 -0
  98. package/dist/validation/rules/event-chain-utils.js.map +1 -0
  99. package/dist/validation/rules/event-flow.d.ts +14 -0
  100. package/dist/validation/rules/event-flow.d.ts.map +1 -0
  101. package/dist/validation/rules/event-flow.js +83 -0
  102. package/dist/validation/rules/event-flow.js.map +1 -0
  103. package/dist/validation/rules/event-graph.d.ts +10 -0
  104. package/dist/validation/rules/event-graph.d.ts.map +1 -0
  105. package/dist/validation/rules/event-graph.js +51 -0
  106. package/dist/validation/rules/event-graph.js.map +1 -0
  107. package/dist/validation/rules/explore-token-pattern.d.ts +10 -0
  108. package/dist/validation/rules/explore-token-pattern.d.ts.map +1 -0
  109. package/dist/validation/rules/explore-token-pattern.js +70 -0
  110. package/dist/validation/rules/explore-token-pattern.js.map +1 -0
  111. package/dist/validation/rules/format-rules.d.ts +10 -0
  112. package/dist/validation/rules/format-rules.d.ts.map +1 -0
  113. package/dist/validation/rules/format-rules.js +61 -0
  114. package/dist/validation/rules/format-rules.js.map +1 -0
  115. package/dist/validation/rules/investigator-token-pattern.d.ts +9 -0
  116. package/dist/validation/rules/investigator-token-pattern.d.ts.map +1 -0
  117. package/dist/validation/rules/investigator-token-pattern.js +39 -0
  118. package/dist/validation/rules/investigator-token-pattern.js.map +1 -0
  119. package/dist/validation/rules/localization-completeness.d.ts +11 -0
  120. package/dist/validation/rules/localization-completeness.d.ts.map +1 -0
  121. package/dist/validation/rules/localization-completeness.js +83 -0
  122. package/dist/validation/rules/localization-completeness.js.map +1 -0
  123. package/dist/validation/rules/mythos-structure.d.ts +10 -0
  124. package/dist/validation/rules/mythos-structure.d.ts.map +1 -0
  125. package/dist/validation/rules/mythos-structure.js +57 -0
  126. package/dist/validation/rules/mythos-structure.js.map +1 -0
  127. package/dist/validation/rules/required-fields.d.ts +9 -0
  128. package/dist/validation/rules/required-fields.d.ts.map +1 -0
  129. package/dist/validation/rules/required-fields.js +38 -0
  130. package/dist/validation/rules/required-fields.js.map +1 -0
  131. package/dist/validation/rules/tile-connectivity.d.ts +9 -0
  132. package/dist/validation/rules/tile-connectivity.d.ts.map +1 -0
  133. package/dist/validation/rules/tile-connectivity.js +125 -0
  134. package/dist/validation/rules/tile-connectivity.js.map +1 -0
  135. package/dist/validation/validator.d.ts +7 -0
  136. package/dist/validation/validator.d.ts.map +1 -0
  137. package/dist/validation/validator.js +35 -0
  138. package/dist/validation/validator.js.map +1 -0
  139. package/package.json +59 -0
  140. package/skills/custom-monsters/SKILL.md +228 -0
  141. package/skills/event-patterns/SKILL.md +268 -0
  142. package/skills/items-and-distribution/SKILL.md +193 -0
  143. package/skills/scenario/SKILL.md +105 -0
  144. package/skills/tile-placement/SKILL.md +163 -0
  145. package/skills/ui-and-puzzles/SKILL.md +347 -0
  146. package/skills/variables-and-mythos/SKILL.md +285 -0
  147. package/src/catalogs/catalog-store.ts +94 -0
  148. package/src/catalogs/catalog-types.ts +10 -0
  149. package/src/catalogs/data/all-catalogs.ts +8391 -0
  150. package/src/catalogs/data/tile-geometry.ts +188 -0
  151. package/src/catalogs/extract-catalogs.ts +172 -0
  152. package/src/index.ts +14 -0
  153. package/src/io/ini-parser.ts +52 -0
  154. package/src/io/ini-writer.ts +51 -0
  155. package/src/io/localization-io.ts +62 -0
  156. package/src/io/package-builder.ts +26 -0
  157. package/src/model/component-types.ts +124 -0
  158. package/src/model/localization-store.ts +52 -0
  159. package/src/model/scenario-model.ts +177 -0
  160. package/src/resources/format-docs.ts +264 -0
  161. package/src/server.ts +336 -0
  162. package/src/tools/lifecycle.ts +183 -0
  163. package/src/tools/map.ts +125 -0
  164. package/src/tools/reference.ts +7 -0
  165. package/src/tools/shared.ts +34 -0
  166. package/src/tools/upsert.ts +133 -0
  167. package/src/validation/rules/catalog-references.ts +95 -0
  168. package/src/validation/rules/cross-references.ts +42 -0
  169. package/src/validation/rules/event-chain-utils.ts +31 -0
  170. package/src/validation/rules/event-flow.ts +93 -0
  171. package/src/validation/rules/event-graph.ts +62 -0
  172. package/src/validation/rules/explore-token-pattern.ts +77 -0
  173. package/src/validation/rules/format-rules.ts +69 -0
  174. package/src/validation/rules/investigator-token-pattern.ts +47 -0
  175. package/src/validation/rules/localization-completeness.ts +93 -0
  176. package/src/validation/rules/mythos-structure.ts +64 -0
  177. package/src/validation/rules/required-fields.ts +43 -0
  178. package/src/validation/rules/tile-connectivity.ts +148 -0
  179. package/src/validation/validator.ts +38 -0
  180. package/tsconfig.json +20 -0
@@ -0,0 +1,61 @@
1
+ const VALID_ROTATIONS = new Set(['0', '90', '180', '270']);
2
+ /**
3
+ * Checks format-level rules:
4
+ * - format must be in range 4-19
5
+ * - type must be 'MoM'
6
+ * - tile rotation must be in {0, 90, 180, 270}
7
+ */
8
+ export function checkFormatRules(model) {
9
+ const results = [];
10
+ const { format, type } = model.questConfig;
11
+ if (format < 4 || format > 19) {
12
+ results.push({
13
+ rule: 'format-rules',
14
+ severity: 'error',
15
+ message: `Quest format ${format} is not in valid range (4-19)`,
16
+ field: 'format',
17
+ });
18
+ }
19
+ if (type !== 'MoM') {
20
+ results.push({
21
+ rule: 'format-rules',
22
+ severity: 'error',
23
+ message: `Quest type "${type}" is not "MoM"`,
24
+ field: 'type',
25
+ });
26
+ }
27
+ for (const comp of model.getByType('Tile')) {
28
+ const rotation = comp.data.rotation;
29
+ if (rotation !== undefined && !VALID_ROTATIONS.has(rotation)) {
30
+ results.push({
31
+ rule: 'format-rules',
32
+ severity: 'warning',
33
+ message: `Tile "${comp.name}" has rotation ${rotation}, expected one of: 0, 90, 180, 270`,
34
+ component: comp.name,
35
+ field: 'rotation',
36
+ });
37
+ }
38
+ }
39
+ // Check buttons vs event refs for Event components
40
+ for (const comp of model.getByType('Event')) {
41
+ const buttons = parseInt(comp.data.buttons ?? '0', 10) || 0;
42
+ let highestEventRef = 0;
43
+ for (let i = 1; i <= 6; i++) {
44
+ const val = comp.data[`event${i}`];
45
+ if (val !== undefined && val !== '') {
46
+ highestEventRef = i;
47
+ }
48
+ }
49
+ if (highestEventRef > buttons) {
50
+ results.push({
51
+ rule: 'format-rules',
52
+ severity: 'error',
53
+ message: `Event "${comp.name}" has buttons=${buttons} but event${highestEventRef} is set — Valkyrie only parses event1..eventN based on buttons count, so event${highestEventRef} will be silently dropped on re-save`,
54
+ component: comp.name,
55
+ field: 'buttons',
56
+ });
57
+ }
58
+ }
59
+ return results;
60
+ }
61
+ //# sourceMappingURL=format-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-rules.js","sourceRoot":"","sources":["../../../src/validation/rules/format-rules.ts"],"names":[],"mappings":"AAGA,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC;IAE3C,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,EAAE,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,gBAAgB,MAAM,+BAA+B;YAC9D,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,eAAe,IAAI,gBAAgB;YAC5C,KAAK,EAAE,MAAM;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;QACpC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,cAAc;gBACpB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,kBAAkB,QAAQ,oCAAoC;gBACzF,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,KAAK,EAAE,UAAU;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,mDAAmD;IACnD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;QAC5D,IAAI,eAAe,GAAG,CAAC,CAAC;QACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACnC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;gBACpC,eAAe,GAAG,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,IAAI,eAAe,GAAG,OAAO,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,cAAc;gBACpB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,UAAU,IAAI,CAAC,IAAI,iBAAiB,OAAO,aAAa,eAAe,iFAAiF,eAAe,sCAAsC;gBACtN,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,KAAK,EAAE,SAAS;aACjB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ValidationResult } from '../../model/component-types.js';
2
+ import type { ScenarioModel } from '../../model/scenario-model.js';
3
+ /**
4
+ * Checks that TokenInvestigators is removed after being added.
5
+ * Community quests always remove TokenInvestigators after the setup phase
6
+ * so the start position marker doesn't stay interactable.
7
+ */
8
+ export declare function checkInvestigatorTokenPattern(model: ScenarioModel): ValidationResult[];
9
+ //# sourceMappingURL=investigator-token-pattern.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"investigator-token-pattern.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/investigator-token-pattern.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAGnE;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAoCtF"}
@@ -0,0 +1,39 @@
1
+ import { parseRefList } from '../../model/component-types.js';
2
+ import { collectEventChain } from './event-chain-utils.js';
3
+ /**
4
+ * Checks that TokenInvestigators is removed after being added.
5
+ * Community quests always remove TokenInvestigators after the setup phase
6
+ * so the start position marker doesn't stay interactable.
7
+ */
8
+ export function checkInvestigatorTokenPattern(model) {
9
+ const results = [];
10
+ // Find events that add TokenInvestigators
11
+ const eventsAddingInv = model.getAll().filter(c => parseRefList(c.data.add ?? '').includes('TokenInvestigators'));
12
+ if (eventsAddingInv.length === 0)
13
+ return results;
14
+ for (const addEvent of eventsAddingInv) {
15
+ // Walk the event chain from the event that adds TokenInvestigators
16
+ const chain = collectEventChain(model, addEvent.name);
17
+ let removesToken = false;
18
+ for (const eventName of chain) {
19
+ const comp = model.get(eventName);
20
+ if (!comp)
21
+ continue;
22
+ if (parseRefList(comp.data.remove ?? '').includes('TokenInvestigators')) {
23
+ removesToken = true;
24
+ break;
25
+ }
26
+ }
27
+ if (!removesToken) {
28
+ results.push({
29
+ rule: 'investigator-token-pattern',
30
+ severity: 'warning',
31
+ message: `"${addEvent.name}" adds TokenInvestigators but the event chain does not remove it — the start position token stays interactable`,
32
+ component: addEvent.name,
33
+ field: 'remove',
34
+ });
35
+ }
36
+ }
37
+ return results;
38
+ }
39
+ //# sourceMappingURL=investigator-token-pattern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"investigator-token-pattern.js","sourceRoot":"","sources":["../../../src/validation/rules/investigator-token-pattern.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAAoB;IAChE,MAAM,OAAO,GAAuB,EAAE,CAAC;IAEvC,0CAA0C;IAC1C,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAChD,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAC9D,CAAC;IAEF,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAEjD,KAAK,MAAM,QAAQ,IAAI,eAAe,EAAE,CAAC;QACvC,mEAAmE;QACnE,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEtD,IAAI,YAAY,GAAG,KAAK,CAAC;QACzB,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;gBACxE,YAAY,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,CAAC;QACH,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,4BAA4B;gBAClC,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,IAAI,QAAQ,CAAC,IAAI,gHAAgH;gBAC1I,SAAS,EAAE,QAAQ,CAAC,IAAI;gBACxB,KAAK,EAAE,QAAQ;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ValidationResult } from '../../model/component-types.js';
2
+ import type { ScenarioModel } from '../../model/scenario-model.js';
3
+ /**
4
+ * Checks localization completeness:
5
+ * - quest.name and quest.description should exist
6
+ * - Events with display != false should have ComponentName.text
7
+ * - Events/Spawns with buttons > 0 should have ComponentName.button1..N
8
+ * - Tokens should have ComponentName.text and ComponentName.button1
9
+ */
10
+ export declare function checkLocalizationCompleteness(model: ScenarioModel): ValidationResult[];
11
+ //# sourceMappingURL=localization-completeness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localization-completeness.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/localization-completeness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAkFtF"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Checks localization completeness:
3
+ * - quest.name and quest.description should exist
4
+ * - Events with display != false should have ComponentName.text
5
+ * - Events/Spawns with buttons > 0 should have ComponentName.button1..N
6
+ * - Tokens should have ComponentName.text and ComponentName.button1
7
+ */
8
+ export function checkLocalizationCompleteness(model) {
9
+ const results = [];
10
+ const loc = model.localization;
11
+ // Check quest-level keys
12
+ if (!loc.has('quest.name')) {
13
+ results.push({
14
+ rule: 'localization-completeness',
15
+ severity: 'warning',
16
+ message: 'Missing localization key "quest.name"',
17
+ });
18
+ }
19
+ if (!loc.has('quest.description')) {
20
+ results.push({
21
+ rule: 'localization-completeness',
22
+ severity: 'warning',
23
+ message: 'Missing localization key "quest.description"',
24
+ });
25
+ }
26
+ for (const comp of model.getAll()) {
27
+ const isEvent = comp.name.startsWith('Event');
28
+ const isSpawn = comp.name.startsWith('Spawn');
29
+ const isToken = comp.name.startsWith('Token');
30
+ if (!isEvent && !isSpawn && !isToken)
31
+ continue;
32
+ const displayExplicitlyFalse = comp.data.display?.toLowerCase() === 'false';
33
+ if (isToken) {
34
+ if (displayExplicitlyFalse)
35
+ continue;
36
+ if (!loc.has(`${comp.name}.text`)) {
37
+ results.push({
38
+ rule: 'localization-completeness',
39
+ severity: 'warning',
40
+ message: `Missing localization key "${comp.name}.text" for token`,
41
+ component: comp.name,
42
+ });
43
+ }
44
+ if (!loc.has(`${comp.name}.button1`)) {
45
+ results.push({
46
+ rule: 'localization-completeness',
47
+ severity: 'warning',
48
+ message: `Missing localization key "${comp.name}.button1" for token`,
49
+ component: comp.name,
50
+ });
51
+ }
52
+ continue;
53
+ }
54
+ // Events and Spawns
55
+ if (displayExplicitlyFalse)
56
+ continue;
57
+ // Check .text key
58
+ if (!loc.has(`${comp.name}.text`)) {
59
+ results.push({
60
+ rule: 'localization-completeness',
61
+ severity: 'warning',
62
+ message: `Missing localization key "${comp.name}.text"`,
63
+ component: comp.name,
64
+ });
65
+ }
66
+ // Check button keys
67
+ const buttons = parseInt(comp.data.buttons ?? '0', 10);
68
+ if (buttons > 0) {
69
+ for (let i = 1; i <= buttons; i++) {
70
+ if (!loc.has(`${comp.name}.button${i}`)) {
71
+ results.push({
72
+ rule: 'localization-completeness',
73
+ severity: 'warning',
74
+ message: `Missing localization key "${comp.name}.button${i}"`,
75
+ component: comp.name,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+ //# sourceMappingURL=localization-completeness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localization-completeness.js","sourceRoot":"","sources":["../../../src/validation/rules/localization-completeness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ValidationResult } from '../../model/component-types.js';
2
+ import type { ScenarioModel } from '../../model/scenario-model.js';
3
+ /**
4
+ * Checks Mythos phase correctness and skill test structure:
5
+ * - Warning: Mythos trigger event without `conditions` — fires every Mythos phase
6
+ * - Warning: No `$end` operation in any event — scenario can never finish
7
+ * - Warning: Event with `quota` but `buttons` != 2 — skill test needs pass/fail buttons
8
+ */
9
+ export declare function checkMythosStructure(model: ScenarioModel): ValidationResult[];
10
+ //# sourceMappingURL=mythos-structure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mythos-structure.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/mythos-structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAsD7E"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Checks Mythos phase correctness and skill test structure:
3
+ * - Warning: Mythos trigger event without `conditions` — fires every Mythos phase
4
+ * - Warning: No `$end` operation in any event — scenario can never finish
5
+ * - Warning: Event with `quota` but `buttons` != 2 — skill test needs pass/fail buttons
6
+ */
7
+ export function checkMythosStructure(model) {
8
+ const results = [];
9
+ const allComponents = model.getAll();
10
+ // Check Mythos events for missing conditions and buttons=0 auto-skip
11
+ for (const comp of allComponents) {
12
+ if (comp.data.trigger === 'Mythos') {
13
+ if (!comp.data.conditions) {
14
+ results.push({
15
+ rule: 'mythos-structure',
16
+ severity: 'warning',
17
+ message: `"${comp.name}" has trigger=Mythos but no conditions — will fire every Mythos phase`,
18
+ component: comp.name,
19
+ field: 'conditions',
20
+ });
21
+ }
22
+ const buttons = comp.data.buttons;
23
+ if (!buttons || buttons === '0') {
24
+ results.push({
25
+ rule: 'mythos-structure',
26
+ severity: 'warning',
27
+ message: `"${comp.name}" has trigger=Mythos with buttons=${buttons ?? '0'} — Valkyrie auto-confirms, skipping sub-events. Set buttons>=1`,
28
+ component: comp.name,
29
+ field: 'buttons',
30
+ });
31
+ }
32
+ }
33
+ }
34
+ // Check for $end operation somewhere in the scenario
35
+ const hasEnd = allComponents.some(c => c.data.operations?.includes('$end'));
36
+ if (!hasEnd) {
37
+ results.push({
38
+ rule: 'mythos-structure',
39
+ severity: 'warning',
40
+ message: 'No event has a $end operation — scenario can never finish',
41
+ });
42
+ }
43
+ // Check skill test events: quota requires buttons=2 (pass/fail)
44
+ for (const comp of allComponents) {
45
+ if (comp.data.quota && comp.data.buttons !== '2') {
46
+ results.push({
47
+ rule: 'mythos-structure',
48
+ severity: 'warning',
49
+ message: `"${comp.name}" has quota=${comp.data.quota} but buttons=${comp.data.buttons ?? '0'} — skill tests need 2 buttons (pass/fail)`,
50
+ component: comp.name,
51
+ field: 'buttons',
52
+ });
53
+ }
54
+ }
55
+ return results;
56
+ }
57
+ //# sourceMappingURL=mythos-structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mythos-structure.js","sourceRoot":"","sources":["../../../src/validation/rules/mythos-structure.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAoB;IACvD,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;IAErC,qEAAqE;IACrE,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,kBAAkB;oBACxB,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,IAAI,IAAI,CAAC,IAAI,uEAAuE;oBAC7F,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,KAAK,EAAE,YAAY;iBACpB,CAAC,CAAC;YACL,CAAC;YAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;YAClC,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;gBAChC,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,kBAAkB;oBACxB,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,IAAI,IAAI,CAAC,IAAI,qCAAqC,OAAO,IAAI,GAAG,gEAAgE;oBACzI,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,KAAK,EAAE,SAAS;iBACjB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,qDAAqD;IACrD,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,kBAAkB;YACxB,QAAQ,EAAE,SAAS;YACnB,OAAO,EAAE,2DAA2D;SACrE,CAAC,CAAC;IACL,CAAC;IAED,gEAAgE;IAChE,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,GAAG,EAAE,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,kBAAkB;gBACxB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,IAAI,IAAI,CAAC,IAAI,eAAe,IAAI,CAAC,IAAI,CAAC,KAAK,gBAAgB,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,GAAG,2CAA2C;gBACvI,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,KAAK,EAAE,SAAS;aACjB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ValidationResult } from '../../model/component-types.js';
2
+ import type { ScenarioModel } from '../../model/scenario-model.js';
3
+ /**
4
+ * Checks for required fields on components:
5
+ * - Tiles must have a `side` field
6
+ * - Events with display not explicitly false must have buttons > 0
7
+ */
8
+ export declare function checkRequiredFields(model: ScenarioModel): ValidationResult[];
9
+ //# sourceMappingURL=required-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"required-fields.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/required-fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAkC5E"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Checks for required fields on components:
3
+ * - Tiles must have a `side` field
4
+ * - Events with display not explicitly false must have buttons > 0
5
+ */
6
+ export function checkRequiredFields(model) {
7
+ const results = [];
8
+ for (const comp of model.getAll()) {
9
+ if (comp.name.startsWith('Tile')) {
10
+ if (!comp.data.side) {
11
+ results.push({
12
+ rule: 'required-fields',
13
+ severity: 'error',
14
+ message: `Tile "${comp.name}" is missing required field "side"`,
15
+ component: comp.name,
16
+ field: 'side',
17
+ });
18
+ }
19
+ }
20
+ if (comp.name.startsWith('Event')) {
21
+ const displayExplicitlyFalse = comp.data.display?.toLowerCase() === 'false';
22
+ if (!displayExplicitlyFalse) {
23
+ const buttons = parseInt(comp.data.buttons ?? '', 10);
24
+ if (isNaN(buttons) || buttons <= 0) {
25
+ results.push({
26
+ rule: 'required-fields',
27
+ severity: 'error',
28
+ message: `Event "${comp.name}" has display enabled but buttons is ${isNaN(buttons) ? 'missing' : buttons}`,
29
+ component: comp.name,
30
+ field: 'buttons',
31
+ });
32
+ }
33
+ }
34
+ }
35
+ }
36
+ return results;
37
+ }
38
+ //# sourceMappingURL=required-fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"required-fields.js","sourceRoot":"","sources":["../../../src/validation/rules/required-fields.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB;IACtD,MAAM,OAAO,GAAuB,EAAE,CAAC;IAEvC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACpB,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,iBAAiB;oBACvB,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,oCAAoC;oBAC/D,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,KAAK,EAAE,MAAM;iBACd,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,OAAO,CAAC;YAC5E,IAAI,CAAC,sBAAsB,EAAE,CAAC;gBAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;gBACtD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;oBACnC,OAAO,CAAC,IAAI,CAAC;wBACX,IAAI,EAAE,iBAAiB;wBACvB,QAAQ,EAAE,OAAO;wBACjB,OAAO,EAAE,UAAU,IAAI,CAAC,IAAI,wCAAwC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE;wBAC1G,SAAS,EAAE,IAAI,CAAC,IAAI;wBACpB,KAAK,EAAE,SAAS;qBACjB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ValidationResult } from '../../model/component-types.js';
2
+ import type { ScenarioModel } from '../../model/scenario-model.js';
3
+ /**
4
+ * Checks that adjacent tiles have at least one matching door/open passage
5
+ * on their shared boundary. Warns when two tiles are placed next to each
6
+ * other but the shared edge is solid wall on either side.
7
+ */
8
+ export declare function checkTileConnectivity(model: ScenarioModel): ValidationResult[];
9
+ //# sourceMappingURL=tile-connectivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tile-connectivity.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/tile-connectivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAqEnE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAwE9E"}
@@ -0,0 +1,125 @@
1
+ import { TILE_GEOMETRY } from '../../catalogs/data/tile-geometry.js';
2
+ /**
3
+ * Apply rotation to get the effective edge for a direction.
4
+ *
5
+ * Rotation is clockwise in degrees. When a tile is rotated:
6
+ * - 90° CW: what was on N is now on E, E→S, S→W, W→N
7
+ * To find effective N, we look at what rotated INTO N, which is W.
8
+ * Effective edge for dir = original edge for the source direction.
9
+ *
10
+ * When edges move between horizontal (N/S) and vertical (E/W) axes,
11
+ * the string must be reversed because the left-to-right reading
12
+ * direction flips relative to the tile's orientation.
13
+ */
14
+ function getEffectiveEdge(geometry, dir, rotation) {
15
+ const steps = ((rotation % 360) + 360) % 360 / 90;
16
+ // Source direction: rotating CW by N steps means dir's content came from
17
+ // the direction N counter-clockwise steps away
18
+ const dirs = ['N', 'E', 'S', 'W'];
19
+ const idx = dirs.indexOf(dir);
20
+ const sourceIdx = ((idx - steps) % 4 + 4) % 4;
21
+ const sourceDir = dirs[sourceIdx];
22
+ const sourceEdge = geometry.edges[sourceDir];
23
+ // Reverse when crossing axes (horizontal↔vertical)
24
+ const isHorizontal = (d) => d === 'N' || d === 'S';
25
+ if (isHorizontal(dir) !== isHorizontal(sourceDir)) {
26
+ return sourceEdge.split('').reverse().join('');
27
+ }
28
+ // Reverse when rotating 180° within the same axis
29
+ if (steps === 2) {
30
+ return sourceEdge.split('').reverse().join('');
31
+ }
32
+ return sourceEdge;
33
+ }
34
+ /**
35
+ * Check whether two facing edges have at least one aligned passage.
36
+ * A passage is 'd' (door) or 'o' (open).
37
+ * Both edges are read in the same coordinate order (left-to-right for N/S,
38
+ * top-to-bottom for E/W), so they align directly at each position.
39
+ */
40
+ function edgesHavePassage(edge1, edge2) {
41
+ const len = Math.min(edge1.length, edge2.length);
42
+ for (let i = 0; i < len; i++) {
43
+ const c1 = edge1[i];
44
+ const c2 = edge2[i];
45
+ if (c1 !== 'w' && c2 !== 'w') {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ /**
52
+ * Checks that adjacent tiles have at least one matching door/open passage
53
+ * on their shared boundary. Warns when two tiles are placed next to each
54
+ * other but the shared edge is solid wall on either side.
55
+ */
56
+ export function checkTileConnectivity(model) {
57
+ const results = [];
58
+ const tiles = [];
59
+ for (const comp of model.getByType('Tile')) {
60
+ const side = comp.data.side;
61
+ const xStr = comp.data.xposition;
62
+ const yStr = comp.data.yposition;
63
+ if (!side || xStr === undefined || yStr === undefined)
64
+ continue;
65
+ const geometry = TILE_GEOMETRY[side];
66
+ if (!geometry)
67
+ continue;
68
+ tiles.push({
69
+ name: comp.name,
70
+ x: parseFloat(xStr),
71
+ y: parseFloat(yStr),
72
+ rotation: parseFloat(comp.data.rotation ?? '0'),
73
+ geometry,
74
+ });
75
+ }
76
+ // Check all pairs for adjacency (tile spacing = 7)
77
+ const TILE_SPACING = 7;
78
+ for (let i = 0; i < tiles.length; i++) {
79
+ for (let j = i + 1; j < tiles.length; j++) {
80
+ const a = tiles[i];
81
+ const b = tiles[j];
82
+ const dx = b.x - a.x;
83
+ const dy = b.y - a.y;
84
+ let dirA = null;
85
+ let dirB = null;
86
+ if (dx === TILE_SPACING && dy === 0) {
87
+ // B is east of A
88
+ dirA = 'E';
89
+ dirB = 'W';
90
+ }
91
+ else if (dx === -TILE_SPACING && dy === 0) {
92
+ // B is west of A
93
+ dirA = 'W';
94
+ dirB = 'E';
95
+ }
96
+ else if (dy === TILE_SPACING && dx === 0) {
97
+ // B is south of A
98
+ dirA = 'S';
99
+ dirB = 'N';
100
+ }
101
+ else if (dy === -TILE_SPACING && dx === 0) {
102
+ // B is north of A
103
+ dirA = 'N';
104
+ dirB = 'S';
105
+ }
106
+ if (!dirA || !dirB)
107
+ continue;
108
+ const edgeA = getEffectiveEdge(a.geometry, dirA, a.rotation);
109
+ const edgeB = getEffectiveEdge(b.geometry, dirB, b.rotation);
110
+ if (!edgesHavePassage(edgeA, edgeB)) {
111
+ const dirLabel = dirA === 'N' || dirA === 'S'
112
+ ? (dirA === 'S' ? 'south-north' : 'north-south')
113
+ : (dirA === 'E' ? 'east-west' : 'west-east');
114
+ results.push({
115
+ rule: 'tile-connectivity',
116
+ severity: 'warning',
117
+ message: `Tiles "${a.name}" and "${b.name}" are adjacent but share no door/open passage on their ${dirLabel} boundary`,
118
+ component: a.name,
119
+ });
120
+ }
121
+ }
122
+ }
123
+ return results;
124
+ }
125
+ //# sourceMappingURL=tile-connectivity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tile-connectivity.js","sourceRoot":"","sources":["../../../src/validation/rules/tile-connectivity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { ValidationResult } from '../model/component-types.js';
2
+ import type { ScenarioModel } from '../model/scenario-model.js';
3
+ /**
4
+ * Runs all validation rules against a scenario model and aggregates results.
5
+ */
6
+ export declare function validateScenario(model: ScenarioModel): ValidationResult[];
7
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AA2BhE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAMzE"}
@@ -0,0 +1,35 @@
1
+ import { checkRequiredFields } from './rules/required-fields.js';
2
+ import { checkCrossReferences } from './rules/cross-references.js';
3
+ import { checkEventGraph } from './rules/event-graph.js';
4
+ import { checkLocalizationCompleteness } from './rules/localization-completeness.js';
5
+ import { checkFormatRules } from './rules/format-rules.js';
6
+ import { checkCatalogReferences } from './rules/catalog-references.js';
7
+ import { checkMythosStructure } from './rules/mythos-structure.js';
8
+ import { checkExploreTokenPattern } from './rules/explore-token-pattern.js';
9
+ import { checkInvestigatorTokenPattern } from './rules/investigator-token-pattern.js';
10
+ import { checkTileConnectivity } from './rules/tile-connectivity.js';
11
+ import { checkEventFlow } from './rules/event-flow.js';
12
+ const ALL_RULES = [
13
+ checkRequiredFields,
14
+ checkCrossReferences,
15
+ checkEventGraph,
16
+ checkLocalizationCompleteness,
17
+ checkFormatRules,
18
+ checkCatalogReferences,
19
+ checkMythosStructure,
20
+ checkExploreTokenPattern,
21
+ checkInvestigatorTokenPattern,
22
+ checkTileConnectivity,
23
+ checkEventFlow,
24
+ ];
25
+ /**
26
+ * Runs all validation rules against a scenario model and aggregates results.
27
+ */
28
+ export function validateScenario(model) {
29
+ const results = [];
30
+ for (const rule of ALL_RULES) {
31
+ results.push(...rule(model));
32
+ }
33
+ return results;
34
+ }
35
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,6BAA6B,EAAE,MAAM,uCAAuC,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,MAAM,SAAS,GAAG;IAChB,mBAAmB;IACnB,oBAAoB;IACpB,eAAe;IACf,6BAA6B;IAC7B,gBAAgB;IAChB,sBAAsB;IACtB,oBAAoB;IACpB,wBAAwB;IACxB,6BAA6B;IAC7B,qBAAqB;IACrB,cAAc;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@thijshakkenberg/valkyrie-mom-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for AI-assisted Mansions of Madness scenario creation with Valkyrie",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "author": {
8
+ "name": "Thijs Hakkenberg"
9
+ },
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/thijs-hakkenberg/ValkyrieMCP.git"
14
+ },
15
+ "keywords": [
16
+ "valkyrie",
17
+ "mansions-of-madness",
18
+ "scenario-editor",
19
+ "mcp",
20
+ "board-game",
21
+ "claude-code-plugin"
22
+ ],
23
+ "files": [
24
+ "dist/",
25
+ "src/",
26
+ ".claude-plugin/",
27
+ ".mcp.json",
28
+ "skills/",
29
+ "agents/",
30
+ "README.md",
31
+ "LICENSE",
32
+ "CLAUDE.md",
33
+ "tsconfig.json"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "start": "node dist/index.js",
38
+ "dev": "tsx src/index.ts",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "extract-catalogs": "tsx src/catalogs/extract-catalogs.ts",
42
+ "lint": "tsc --noEmit",
43
+ "convert-tiles": "tsx scripts/convert-tile-images.ts",
44
+ "prepublishOnly": "npm run lint && npm run test && npm run build"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.12.1",
48
+ "archiver": "^7.0.1",
49
+ "zod": "^3.24.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/archiver": "^6.0.3",
53
+ "@types/node": "^22.13.4",
54
+ "@vitest/coverage-v8": "^3.2.4",
55
+ "tsx": "^4.19.2",
56
+ "typescript": "^5.7.3",
57
+ "vitest": "^3.0.5"
58
+ }
59
+ }