@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,17 @@
1
+ {
2
+ "name": "valkyrie-mom",
3
+ "version": "1.0.0",
4
+ "description": "AI-assisted Mansions of Madness scenario editor for Valkyrie",
5
+ "author": {
6
+ "name": "Thijs Hakkenberg"
7
+ },
8
+ "repository": "https://github.com/thijs-hakkenberg/ValkyrieMCP",
9
+ "keywords": ["valkyrie", "mansions-of-madness", "scenario-editor", "mcp", "board-game"],
10
+ "mcpServers": {
11
+ "valkyrie-mom": {
12
+ "command": "npx",
13
+ "args": ["tsx", "${CLAUDE_PLUGIN_ROOT}/src/index.ts"],
14
+ "cwd": "${CLAUDE_PLUGIN_ROOT}"
15
+ }
16
+ }
17
+ }
package/.mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "valkyrie-mom": {
4
+ "command": "npx",
5
+ "args": ["tsx", "${CLAUDE_PLUGIN_ROOT}/src/index.ts"],
6
+ "cwd": "${CLAUDE_PLUGIN_ROOT}"
7
+ }
8
+ }
9
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,73 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm test # Run all tests (vitest)
9
+ npx vitest run tests/tools/map.test.ts # Run a single test file
10
+ npx vitest run -t "round-trips" # Run tests matching a name pattern
11
+ npm run test:watch # Watch mode
12
+ npm run lint # Type check (tsc --noEmit)
13
+ npm run build # Compile to dist/
14
+ npx tsx src/index.ts # Run MCP server locally via stdio
15
+ ```
16
+
17
+ ## What This Project Is
18
+
19
+ An MCP (Model Context Protocol) server that exposes Valkyrie scenario editing as tools. AI assistants call these tools to create, modify, validate, and build Mansions of Madness 2nd Edition scenarios for the [Valkyrie](https://github.com/NPBruce/valkyrie) app.
20
+
21
+ ## Architecture
22
+
23
+ **ESM project** (`"type": "module"` in package.json). All imports use `.js` extensions which tsx resolves to `.ts` at runtime.
24
+
25
+ ### Data Flow
26
+
27
+ ```
28
+ Valkyrie INI files on disk
29
+ ↓ loadScenario() [src/tools/lifecycle.ts]
30
+ ↓ parseIni() [src/io/ini-parser.ts] + parseLocalization() [src/io/localization-io.ts]
31
+ ScenarioModel (in-memory) [src/model/scenario-model.ts]
32
+ ↓ upsert/delete/validate via MCP tool calls
33
+ ↓ saveScenario() → writeIni() + writeLocalization()
34
+ Valkyrie INI files on disk
35
+ ↓ buildScenario() → buildPackage() [src/io/package-builder.ts]
36
+ .valkyrie ZIP archive
37
+ ```
38
+
39
+ ### Key Design Decisions
40
+
41
+ - **Singleton model**: `server.ts` holds one `currentModel: ScenarioModel | null`. All tools operate on it. `load_scenario` or `create_scenario` sets it.
42
+ - **Component-to-file mapping**: Component names determine their INI file via prefix matching (`Event*` → `events.ini`, `Tile*` → `tiles.ini`, etc.). Defined in `COMPONENT_FILE_MAP` in `component-types.ts`.
43
+ - **Reference tracking**: Fields `event1`..`event6`, `add`, `remove`, `monster` contain space-separated component names. Deleting a component cascades: its name is removed from all reference fields in other components.
44
+ - **Validation is rule-based**: Each rule in `src/validation/rules/` is an independent function `(model) => ValidationResult[]`. The orchestrator in `validator.ts` runs them all.
45
+
46
+ ### Valkyrie INI Format Quirks
47
+
48
+ The INI parser/writer handle Valkyrie-specific behavior:
49
+ - `[QuestData]` and `[QuestText]` are "bare-key" sections: they list filenames without `key=value` syntax.
50
+ - Values split on first `=` only, so `operations=$end,=,1` is valid (key=`operations`, value=`$end,=,1`).
51
+ - Localization CSVs use `.,English` as the header line and optionally quote values containing commas.
52
+
53
+ ### Valkyrie Editor Paths
54
+
55
+ The MCP server auto-detects where Valkyrie stores scenarios (`getEditorDir()` in `lifecycle.ts`):
56
+ - macOS/Linux: `~/.config/Valkyrie/MoM/Editor/`
57
+ - Windows: `%APPDATA%\Valkyrie\MoM\Editor\`
58
+
59
+ This mirrors `Game.DefaultAppData()` from the Valkyrie C# source.
60
+
61
+ ## Testing Conventions
62
+
63
+ - TDD approach: tests mirror source structure (`tests/io/`, `tests/model/`, `tests/tools/`, `tests/validation/`).
64
+ - `tests/fixtures/ExoticMaterial/` is the golden reference scenario (complete, community-authored, format=18). Golden tests load it, validate (0 errors expected), re-serialize, and compare.
65
+ - `tests/integration.test.ts` exercises the full create → populate → validate → build pipeline.
66
+ - Tool-level tests in `tests/tools/` test the business logic functions directly, not the MCP protocol layer.
67
+
68
+ ## Valkyrie Source Reference
69
+
70
+ The Valkyrie Unity app source is at `/Users/hakketh/projects/repos/valkyrie/`. Key files for understanding the scenario format:
71
+ - `unity/Assets/Scripts/Quest/QuestData.cs` — all component types, fields, parsing logic
72
+ - `unity/Assets/Scripts/Game.cs` — `DefaultAppData()` path logic
73
+ - `unity/Assets/Scripts/Content/QuestLoader.cs` — editor path construction
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 thijs-hakkenberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # valkyrie-mom-mcp
2
+
3
+ MCP (Model Context Protocol) server for AI-assisted Mansions of Madness 2nd Edition scenario creation with [Valkyrie](https://github.com/NPBruce/valkyrie).
4
+
5
+ ## What it does
6
+
7
+ This server exposes Valkyrie scenario editing as MCP tools, enabling AI assistants (Claude, etc.) to create, modify, validate, and build complete MoM scenarios. It auto-detects the Valkyrie editor directory so scenarios appear directly in the app.
8
+
9
+ ## Prerequisites
10
+
11
+ - [Node.js](https://nodejs.org/) >= 18
12
+ - [Valkyrie](https://github.com/NPBruce/valkyrie) (optional, for playing built scenarios)
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ npm install
18
+ ```
19
+
20
+ ### As an MCP server (Claude Code, Claude Desktop, etc.)
21
+
22
+ Add to your `.mcp.json` or MCP client config:
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "valkyrie-mom": {
28
+ "command": "npx",
29
+ "args": ["tsx", "src/index.ts"],
30
+ "cwd": "/path/to/valkyrie_mom_mcp"
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### Development
37
+
38
+ ```bash
39
+ npm test # Run all tests
40
+ npm run test:watch # Watch mode
41
+ npm run lint # Type check
42
+ npm run build # Compile to dist/
43
+ ```
44
+
45
+ ## MCP Tools
46
+
47
+ ### Lifecycle
48
+ | Tool | Description |
49
+ |------|-------------|
50
+ | `list_scenarios` | List all scenarios in the Valkyrie editor directory |
51
+ | `create_scenario` | Create a new scenario (defaults to Valkyrie editor dir) |
52
+ | `load_scenario` | Load an existing scenario from a directory |
53
+ | `get_scenario_state` | Get current scenario component/localization summary |
54
+ | `validate_scenario` | Run all validation rules |
55
+ | `build_scenario` | Save and build `.valkyrie` package |
56
+
57
+ ### Components
58
+ | Tool | Description |
59
+ |------|-------------|
60
+ | `upsert_event` | Create or update an event |
61
+ | `upsert_tile` | Create or update a tile |
62
+ | `upsert_token` | Create or update a token |
63
+ | `upsert_spawn` | Create or update a monster spawn |
64
+ | `upsert_item` | Create or update an item |
65
+ | `upsert_puzzle` | Create or update a puzzle |
66
+ | `upsert_ui` | Create or update a UI element |
67
+ | `delete_component` | Delete a component with cascade reference cleanup |
68
+ | `set_localization` | Set localization key-value pairs |
69
+
70
+ ### Map
71
+ | Tool | Description |
72
+ |------|-------------|
73
+ | `get_map_ascii` | Render tile layout as ASCII art |
74
+ | `suggest_tile_layout` | Suggest coordinates for linear, L-shape, or hub-spoke layouts |
75
+ | `place_tile_relative` | Compute position relative to an existing tile |
76
+
77
+ ### Reference
78
+ | Tool | Description |
79
+ |------|-------------|
80
+ | `search_game_content` | Search game content catalogs |
81
+
82
+ ## MCP Resources
83
+
84
+ | Resource | URI |
85
+ |----------|-----|
86
+ | Event format docs | `valkyrie://format/events` |
87
+ | Localization format docs | `valkyrie://format/localization` |
88
+ | Component format docs | `valkyrie://format/components` |
89
+ | Current scenario state | `valkyrie://scenario/current` |
90
+
91
+ ## MCP Prompts
92
+
93
+ | Prompt | Description |
94
+ |--------|-------------|
95
+ | `create-scenario` | Guided workflow for creating a new scenario |
96
+ | `review-scenario` | Analyze a scenario for balance and completeness |
97
+
98
+ ## Valkyrie Editor Paths
99
+
100
+ The server auto-detects where Valkyrie stores editor scenarios:
101
+
102
+ | Platform | Path |
103
+ |----------|------|
104
+ | macOS / Linux | `~/.config/Valkyrie/MoM/Editor/` |
105
+ | Windows | `%APPDATA%\Valkyrie\MoM\Editor\` |
106
+
107
+ ## Scenario File Format
108
+
109
+ A scenario is a directory containing:
110
+
111
+ - `quest.ini` - Quest config and file listings
112
+ - `events.ini` - Event components (triggers, buttons, branching)
113
+ - `tiles.ini` - Map tile placements
114
+ - `tokens.ini` - Search, explore, interact, and wall tokens
115
+ - `spawns.ini` - Monster spawn configurations
116
+ - `items.ini` - Quest items
117
+ - `ui.ini` - UI elements (backgrounds, etc.)
118
+ - `other.ini` - Puzzles and misc components
119
+ - `Localization.English.txt` - CSV localization (key,value pairs)
120
+
121
+ Built scenarios are ZIP archives with a `.valkyrie` extension.
122
+
123
+ ## Validation Rules
124
+
125
+ The server validates scenarios against 6 rule sets:
126
+
127
+ 1. **Unique names** - No duplicate component names
128
+ 2. **Required fields** - Tiles have `side`, displayed events have `buttons`
129
+ 3. **Cross-references** - All referenced components exist (built-in game content like `Monster*` is exempt)
130
+ 4. **Event graph** - `EventStart` trigger exists, no unreachable/dead-end events
131
+ 5. **Localization completeness** - `quest.name`, `quest.description`, event text, button labels
132
+ 6. **Format rules** - Valid format version, type=MoM, tile rotations in {0, 90, 180, 270}
133
+
134
+ ## Project Structure
135
+
136
+ ```
137
+ src/
138
+ io/ INI parser/writer, localization CSV, ZIP packager
139
+ model/ ScenarioModel, LocalizationStore, component types
140
+ validation/ 6 rules + orchestrator
141
+ rules/ Individual validation rule implementations
142
+ tools/ MCP tool implementations
143
+ resources/ Format documentation resources
144
+ server.ts MCP server registration
145
+ index.ts Entry point (stdio transport)
146
+ tests/
147
+ fixtures/ ExoticMaterial (reference scenario), MinimalScenario
148
+ io/ IO layer tests
149
+ model/ Model layer tests
150
+ tools/ Tool tests
151
+ validation/ Validation rule tests
152
+ golden.test.ts Round-trip integrity tests
153
+ integration.test.ts Full pipeline tests
154
+ ```
155
+
156
+ ## Related Projects
157
+
158
+ - [Valkyrie](https://github.com/NPBruce/valkyrie) - Scenario builder and player
159
+ - [valkyrie-questdata](https://github.com/NPBruce/valkyrie-questdata) - Community scenario source data
160
+ - [valkyrie-store](https://github.com/NPBruce/valkyrie-store) - Scenario manifest pipeline
161
+
162
+ ## License
163
+
164
+ ISC
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: scenario-designer
3
+ model: inherit
4
+ color: green
5
+ description: |
6
+ Autonomous Mansions of Madness scenario designer. Creates complete, playable scenarios for the Valkyrie app using MCP tools and advanced patterns.
7
+
8
+ <example>
9
+ Context: User wants to create a new scenario from scratch
10
+ user: "Create a haunted mansion scenario with 4 tiles and a boss fight"
11
+ </example>
12
+
13
+ <example>
14
+ Context: User wants to build on an existing concept
15
+ user: "Design a scenario set in Innsmouth with Deep One encounters and a ritual finale"
16
+ </example>
17
+
18
+ <example>
19
+ Context: User wants a specific gameplay mechanic
20
+ user: "Build a scenario with an interactive journal puzzle and mythos scaling"
21
+ </example>
22
+ skills:
23
+ - scenario
24
+ - event-patterns
25
+ - tile-placement
26
+ - variables-and-mythos
27
+ - custom-monsters
28
+ - ui-and-puzzles
29
+ - items-and-distribution
30
+ ---
31
+
32
+ # Scenario Designer Agent
33
+
34
+ You are an autonomous Mansions of Madness 2nd Edition scenario designer. You create complete, playable scenarios for the Valkyrie app using MCP tools.
35
+
36
+ ## Your Capabilities
37
+
38
+ - Design tile layouts with proper spatial relationships and placement chains
39
+ - Create event chains with branching narrative paths, loops, and dialogues
40
+ - Place tokens for exploration, searches, and encounters
41
+ - Configure monster spawns with appropriate difficulty scaling
42
+ - Build custom puzzles and UI overlays
43
+ - Set up mythos scaling for tension progression
44
+ - Write atmospheric narrative text in the Lovecraftian horror style
45
+ - Validate scenarios for correctness and completeness
46
+
47
+ ## Available Skills
48
+
49
+ Use these skills for detailed pattern guidance:
50
+ - `/scenario` — End-to-end creation workflow
51
+ - `/event-patterns` — Loops, dialogues, silent events, token swaps, random events, branching
52
+ - `/tile-placement` — Placement chains, multi-entry tiles, naming conventions
53
+ - `/variables-and-mythos` — Variable types, mythos scaling formula, random generation, hero detection
54
+ - `/custom-monsters` — Custom activations, evade/horror events, spawn triggering
55
+ - `/ui-and-puzzles` — Prologues, interactive journals, combination locks, built-in puzzles
56
+ - `/items-and-distribution` — Random items, unique items, starting items, inspection events
57
+
58
+ ## Design Principles
59
+
60
+ ### Map Design
61
+ - Start small (2-3 tiles), expand through exploration
62
+ - Use the standard placement chain: PlaceTile → PlaceDecoration → PlaceConnections → PlaceItems → PlacePeople → MoveOneSpace
63
+ - Place explore tokens at tile boundaries to gate progression
64
+ - Use walls to create interesting pathways
65
+ - Standard tile spacing: 7 units
66
+ - Guard multi-entry tiles with `vartests` on a revealed flag
67
+
68
+ ### Event Flow
69
+ - Always have `EventStart` with `trigger=EventStart`
70
+ - Use `display=false` for chaining events without dialog
71
+ - Silent events with `event1` MUST have `buttons>=1`
72
+ - Use `randomevents=true` for random encounter pools
73
+ - End scenarios with `operations=$end,=,1`
74
+ - Include both victory and defeat paths
75
+ - Use vartests for branching: button1=FAIL, button2=PASS
76
+
77
+ ### Difficulty Balance
78
+ - Scale monsters per hero count with `uniquehealthhero`
79
+ - Provide 1 search token per area for item discovery
80
+ - Give starting items matching investigator count
81
+ - Mythos scaling formula: `deadlyRound = 20 - #heroes`, `majorRound = deadlyRound / 2`
82
+ - Initialize mythos variables in EventStart chain
83
+ - Use `>=` not `==` for round checks to avoid missed triggers
84
+ - Use one-shot flags (`conditions`) for events that should fire once
85
+
86
+ ### Narrative Style
87
+ - Use italics `<i>text</i>` for atmospheric descriptions
88
+ - Use bold `<b>text</b>` for important instructions
89
+ - Reference game elements: `{ffg:MONSTER_NAME}`, `{ffg:TILE_NAME}`
90
+ - Keep button text concise: `{qst:CONTINUE}`, `{qst:PASS}`, `{qst:FAIL}`
91
+ - Use `{rnd:hero}` for random hero selection, `{c:EventName}` for recall
92
+
93
+ ### UI & Puzzles
94
+ - Always use `vunits=True` for resolution independence
95
+ - Layering order matters: background first, buttons LAST
96
+ - Clean up all UI elements when dismissing overlays
97
+
98
+ ## Workflow
99
+
100
+ 1. Use `create_scenario` to start
101
+ 2. Design map with `upsert_tile` and `suggest_tile_layout`
102
+ 3. Build the tile placement chains for each area
103
+ 4. Build event chain with `upsert_event` (start → explore → encounter → finale)
104
+ 5. Set up mythos scaling with StartRound triggers
105
+ 6. Place tokens with `upsert_token`
106
+ 7. Add spawns with `upsert_spawn`
107
+ 8. Configure items with `upsert_item`
108
+ 9. Write all narrative with `set_localization`
109
+ 10. Validate with `validate_scenario`
110
+ 11. Build with `build_scenario`
111
+
112
+ ## Available MCP Tools
113
+
114
+ All tools from the `valkyrie-mom` MCP server are available. Use `get_map_ascii` frequently to verify spatial layout. Run `validate_scenario` after major changes. Use `search_game_content` to find valid catalog entries for tiles, monsters, and items.
@@ -0,0 +1,20 @@
1
+ import type { CatalogEntry, CatalogType } from './catalog-types.js';
2
+ export declare class CatalogStore {
3
+ private entries;
4
+ private byId;
5
+ private byType;
6
+ constructor();
7
+ /**
8
+ * Search catalog entries by case-insensitive substring match on id, name, or traits.
9
+ * Optionally filter by CatalogType.
10
+ */
11
+ search(query: string, type?: CatalogType): CatalogEntry[];
12
+ /** Get a catalog entry by exact ID. */
13
+ getById(id: string): CatalogEntry | undefined;
14
+ /** Get all entries of a given type. */
15
+ getByType(type: CatalogType): CatalogEntry[];
16
+ /** Get a Set of all entry IDs, optionally filtered by type. */
17
+ getAllIds(type?: CatalogType): Set<string>;
18
+ }
19
+ export declare function getSharedCatalog(): CatalogStore;
20
+ //# sourceMappingURL=catalog-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-store.d.ts","sourceRoot":"","sources":["../../src/catalogs/catalog-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAYpE,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,IAAI,CAA4B;IACxC,OAAO,CAAC,MAAM,CAAmC;;IAoCjD;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,YAAY,EAAE;IAazD,uCAAuC;IACvC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI7C,uCAAuC;IACvC,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,YAAY,EAAE;IAI5C,+DAA+D;IAC/D,SAAS,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC;CAI3C;AAKD,wBAAgB,gBAAgB,IAAI,YAAY,CAK/C"}
@@ -0,0 +1,78 @@
1
+ import { MONSTERS, TILES, AUDIO, ITEMS, INVESTIGATORS, PUZZLES, TOKENS, } from './data/all-catalogs.js';
2
+ import { TILE_GEOMETRY } from './data/tile-geometry.js';
3
+ export class CatalogStore {
4
+ entries;
5
+ byId;
6
+ byType;
7
+ constructor() {
8
+ this.entries = [
9
+ ...MONSTERS,
10
+ ...TILES,
11
+ ...AUDIO,
12
+ ...ITEMS,
13
+ ...INVESTIGATORS,
14
+ ...PUZZLES,
15
+ ...TOKENS,
16
+ ];
17
+ // Merge tile geometry data into tile entries
18
+ for (const entry of this.entries) {
19
+ if (entry.type === 'tile') {
20
+ const geo = TILE_GEOMETRY[entry.id];
21
+ if (geo) {
22
+ entry.grid = geo.grid;
23
+ entry.edges = geo.edges;
24
+ entry.desc = geo.desc;
25
+ }
26
+ }
27
+ }
28
+ this.byId = new Map();
29
+ this.byType = new Map();
30
+ for (const entry of this.entries) {
31
+ this.byId.set(entry.id, entry);
32
+ const typeList = this.byType.get(entry.type) ?? [];
33
+ typeList.push(entry);
34
+ this.byType.set(entry.type, typeList);
35
+ }
36
+ }
37
+ /**
38
+ * Search catalog entries by case-insensitive substring match on id, name, or traits.
39
+ * Optionally filter by CatalogType.
40
+ */
41
+ search(query, type) {
42
+ const q = query.toLowerCase();
43
+ const source = type ? (this.byType.get(type) ?? []) : this.entries;
44
+ return source.filter(entry => {
45
+ if (entry.id.toLowerCase().includes(q))
46
+ return true;
47
+ if (entry.name.toLowerCase().includes(q))
48
+ return true;
49
+ if (entry.traits.some(t => t.toLowerCase().includes(q)))
50
+ return true;
51
+ if (typeof entry.desc === 'string' && entry.desc.toLowerCase().includes(q))
52
+ return true;
53
+ return false;
54
+ });
55
+ }
56
+ /** Get a catalog entry by exact ID. */
57
+ getById(id) {
58
+ return this.byId.get(id);
59
+ }
60
+ /** Get all entries of a given type. */
61
+ getByType(type) {
62
+ return this.byType.get(type) ?? [];
63
+ }
64
+ /** Get a Set of all entry IDs, optionally filtered by type. */
65
+ getAllIds(type) {
66
+ const source = type ? (this.byType.get(type) ?? []) : this.entries;
67
+ return new Set(source.map(e => e.id));
68
+ }
69
+ }
70
+ /** Shared singleton instance — avoids re-indexing 846 entries multiple times */
71
+ let sharedInstance = null;
72
+ export function getSharedCatalog() {
73
+ if (!sharedInstance) {
74
+ sharedInstance = new CatalogStore();
75
+ }
76
+ return sharedInstance;
77
+ }
78
+ //# sourceMappingURL=catalog-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-store.js","sourceRoot":"","sources":["../../src/catalogs/catalog-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export type CatalogType = 'monster' | 'tile' | 'audio' | 'item' | 'investigator' | 'puzzle' | 'token';
2
+ export interface CatalogEntry {
3
+ id: string;
4
+ type: CatalogType;
5
+ name: string;
6
+ pack: string;
7
+ traits: string[];
8
+ [key: string]: unknown;
9
+ }
10
+ //# sourceMappingURL=catalog-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-types.d.ts","sourceRoot":"","sources":["../../src/catalogs/catalog-types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtG,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=catalog-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-types.js","sourceRoot":"","sources":["../../src/catalogs/catalog-types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import type { CatalogEntry } from '../catalog-types.js';
2
+ export declare const MONSTERS: CatalogEntry[];
3
+ export declare const TILES: CatalogEntry[];
4
+ export declare const AUDIO: CatalogEntry[];
5
+ export declare const ITEMS: CatalogEntry[];
6
+ export declare const INVESTIGATORS: CatalogEntry[];
7
+ export declare const PUZZLES: CatalogEntry[];
8
+ export declare const TOKENS: CatalogEntry[];
9
+ //# sourceMappingURL=all-catalogs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"all-catalogs.d.ts","sourceRoot":"","sources":["../../../src/catalogs/data/all-catalogs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,eAAO,MAAM,QAAQ,EAAE,YAAY,EAoUlC,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,YAAY,EAmgE/B,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,YAAY,EA+5C/B,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,YAAY,EA2kD/B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,YAAY,EAyNvC,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,YAAY,EAyDjC,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,YAAY,EAonFhC,CAAC"}