@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,228 @@
1
+ ---
2
+ name: custom-monsters
3
+ description: Custom monster creation for Valkyrie MoM. Use when creating scenario-specific enemies with custom activations, evade, horror, and spawn triggering.
4
+ ---
5
+
6
+ # /custom-monsters - Custom Monster Creation
7
+
8
+ Create scenario-specific monsters with unique behaviors, activations, and stat overrides.
9
+
10
+ ## Component Structure
11
+
12
+ Custom monsters use the `CustomMonster` prefix and require a `base` field referencing a catalog monster.
13
+
14
+ ```
15
+ upsert_token("CustomMonsterBoss", {
16
+ base: "MonsterCultist",
17
+ health: "8",
18
+ healthperhero: "2"
19
+ })
20
+ ```
21
+
22
+ **Note:** Custom monsters are created via the spawn system. The `base` field determines the visual appearance and default stats. Override any field to customize.
23
+
24
+ ### Required Fields
25
+
26
+ | Field | Description |
27
+ |-------|-------------|
28
+ | `base` | Base monster type from catalog (e.g., MonsterCultist, MonsterGhost) |
29
+
30
+ ### Optional Stat Overrides
31
+
32
+ | Field | Description |
33
+ |-------|-------------|
34
+ | `health` | Base health points |
35
+ | `healthperhero` | Additional health per investigator |
36
+ | `horror` | Horror value for horror checks |
37
+ | `awareness` | Detection range |
38
+ | `traits` | Space-separated trait keywords |
39
+
40
+ ## Custom Activations
41
+
42
+ Override the default monster activation with a custom event sequence. When the monster activates during the mythos phase, your custom event fires instead of the standard AI behavior.
43
+
44
+ ### Activation Event Structure
45
+
46
+ ```
47
+ # Monster definition with custom activation
48
+ upsert_spawn("SpawnBoss", {
49
+ monster: "CustomMonsterCultLeader",
50
+ uniquehealth: "10",
51
+ uniquehealthhero: "3"
52
+ })
53
+
54
+ # Activation event — triggered when the monster activates
55
+ upsert_event("EventCultLeaderActivation", {
56
+ trigger: "Mythos",
57
+ buttons: "2",
58
+ randomevents: "true",
59
+ event1: "EventCultLeaderAttack",
60
+ event2: "EventCultLeaderSummon"
61
+ })
62
+
63
+ # Attack pattern
64
+ upsert_event("EventCultLeaderAttack", {
65
+ buttons: "1",
66
+ event1: "EventCultLeaderMoveAndStrike"
67
+ })
68
+
69
+ # Summon pattern — spawns a minion
70
+ upsert_event("EventCultLeaderSummon", {
71
+ buttons: "1",
72
+ add: "SpawnCultistMinion",
73
+ event1: "EventCultLeaderDone"
74
+ })
75
+ ```
76
+
77
+ ### Random Move + Attack Patterns
78
+
79
+ Use `randomevents=true` to vary monster behavior each activation:
80
+
81
+ ```
82
+ upsert_event("EventBossActivation", {
83
+ display: "false",
84
+ buttons: "3",
85
+ randomevents: "true",
86
+ event1: "EventBossAggressive", # charge and attack
87
+ event2: "EventBossDefensive", # retreat and heal
88
+ event3: "EventBossSpecial" # unique ability
89
+ })
90
+ ```
91
+
92
+ ## Evade & Horror Events
93
+
94
+ ### Custom Evade
95
+
96
+ When an investigator attempts to evade, the custom evade event fires:
97
+
98
+ ```
99
+ upsert_event("EventBossEvade", {
100
+ buttons: "2",
101
+ quota: "2", # difficulty threshold
102
+ event1: "EventBossEvadeSuccess", # pass → escape
103
+ event2: "EventBossEvadeFail" # fail → consequence
104
+ })
105
+
106
+ set_localization({
107
+ "EventBossEvade.text": "The cult leader blocks your path. Test {agility} to slip past.",
108
+ "EventBossEvade.button1": "{qst:PASS}",
109
+ "EventBossEvade.button2": "{qst:FAIL}",
110
+ "EventBossEvadeSuccess.text": "You dart past the robed figure.",
111
+ "EventBossEvadeFail.text": "The cult leader grabs your arm. Suffer 1 damage."
112
+ })
113
+ ```
114
+
115
+ ### Custom Horror
116
+
117
+ When an investigator encounters the monster, the custom horror event fires:
118
+
119
+ ```
120
+ upsert_event("EventBossHorror", {
121
+ buttons: "2",
122
+ quota: "1",
123
+ event1: "EventBossHorrorPass",
124
+ event2: "EventBossHorrorFail"
125
+ })
126
+
127
+ set_localization({
128
+ "EventBossHorror.text": "The creature's true form is revealed. Test {will} to resist the madness.",
129
+ "EventBossHorror.button1": "{qst:PASS}",
130
+ "EventBossHorror.button2": "{qst:FAIL}"
131
+ })
132
+ ```
133
+
134
+ ## Spawn Triggering
135
+
136
+ ### Round-Based Spawning
137
+
138
+ Use `EndRound` trigger with `#round` vartests and one-shot flags:
139
+
140
+ ```
141
+ # Check if it's time to spawn
142
+ upsert_event("EventRound3Spawn", {
143
+ trigger: "EndRound",
144
+ display: "false",
145
+ buttons: "2",
146
+ conditions: "round3Spawned,==,0",
147
+ vartests: "VarOperation:#round,>=,3",
148
+ event1: "EventRound3SpawnSkip", # not time yet
149
+ event2: "EventRound3SpawnFire" # round >= 3, spawn!
150
+ })
151
+
152
+ upsert_event("EventRound3SpawnFire", {
153
+ buttons: "1",
154
+ operations: "round3Spawned,=,1",
155
+ add: "SpawnHallwayGhost",
156
+ event1: "EventRound3SpawnNarrative"
157
+ })
158
+ ```
159
+
160
+ ### Progressive Spawning
161
+
162
+ Spawn increasingly dangerous monsters as the scenario progresses:
163
+
164
+ ```
165
+ # Early game — minor enemies
166
+ upsert_event("EventEarlySpawn", {
167
+ trigger: "EndRound",
168
+ display: "false",
169
+ buttons: "2",
170
+ conditions: "earlySpawnDone,==,0",
171
+ vartests: "VarOperation:#round,>=,2",
172
+ event1: "EventEarlySpawnSkip",
173
+ event2: "EventEarlySpawnFire"
174
+ })
175
+
176
+ # Mid game — tougher enemies
177
+ upsert_event("EventMidSpawn", {
178
+ trigger: "EndRound",
179
+ display: "false",
180
+ buttons: "2",
181
+ conditions: "midSpawnDone,==,0",
182
+ vartests: "VarOperation:#round,>=,6",
183
+ event1: "EventMidSpawnSkip",
184
+ event2: "EventMidSpawnFire"
185
+ })
186
+
187
+ # Late game — boss
188
+ upsert_event("EventBossSpawn", {
189
+ trigger: "EndRound",
190
+ display: "false",
191
+ buttons: "2",
192
+ conditions: "bossSpawnDone,==,0",
193
+ vartests: "VarOperation:#round,>=,10",
194
+ event1: "EventBossSpawnSkip",
195
+ event2: "EventBossSpawnFire"
196
+ })
197
+ ```
198
+
199
+ ### Event-Triggered Spawns
200
+
201
+ Spawn monsters in response to player actions:
202
+
203
+ ```
204
+ upsert_event("EventOpenCoffin", {
205
+ buttons: "1",
206
+ add: "SpawnCoffinZombie",
207
+ event1: "EventCoffinNarrative"
208
+ })
209
+ ```
210
+
211
+ ## Monster Stats Reference
212
+
213
+ Common base monsters for the `base` field:
214
+
215
+ | Monster | Health | Per Hero | Notes |
216
+ |---------|--------|----------|-------|
217
+ | MonsterCultist | 2 | 1 | Basic human enemy |
218
+ | MonsterGhost | 3 | 1 | Incorporeal, horror focus |
219
+ | MonsterZombie | 3 | 1 | Slow but tough |
220
+ | MonsterManiac | 2 | 1 | Fast, aggressive |
221
+ | MonsterWitch | 3 | 1 | Spell-based attacks |
222
+ | MonsterHuntingHorror | 5 | 2 | Flying, strong |
223
+ | MonsterRiotOfFlesh | 4 | 2 | Resilient |
224
+ | MonsterStarSpawn | 6 | 2 | Boss-tier |
225
+ | MonsterDeepOne | 3 | 1 | Aquatic areas |
226
+ | MonsterThrall | 2 | 1 | Swarm enemy |
227
+
228
+ Use `search_game_content` with query "Monster" to find all available monsters including expansion content.
@@ -0,0 +1,268 @@
1
+ ---
2
+ name: event-patterns
3
+ description: Complex event flow patterns for Valkyrie MoM scenarios. Use when creating event loops, multi-question dialogues, silent events, token swaps, random events, or variable-controlled branching.
4
+ ---
5
+
6
+ # /event-patterns - Complex Event Flow Patterns
7
+
8
+ Advanced event patterns for Mansions of Madness scenarios. Each pattern includes structure, rules, and MCP tool call examples.
9
+
10
+ ## Silent Events
11
+
12
+ Silent events (`display=false`) auto-advance without showing dialog. Used for logic branching, cleanup, and chaining.
13
+
14
+ **Critical rules:**
15
+ - If a silent event has `event1` set, it MUST have `buttons>=1`. Valkyrie only parses `event1..eventN` up to the `buttons` count.
16
+ - A silent event with `buttons=0` is only safe when it has NO `eventN` fields (e.g., a terminal remove-only event).
17
+ - Silent events with `vartests` branch based on variable state without player interaction.
18
+
19
+ **Example — Silent cleanup event:**
20
+ ```
21
+ upsert_event("EventCleanup", {
22
+ display: "false",
23
+ buttons: "1",
24
+ remove: "TokenOldClue SpawnGuard",
25
+ event1: "EventNextPhase"
26
+ })
27
+ ```
28
+
29
+ **Example — Terminal silent event (no chaining):**
30
+ ```
31
+ upsert_event("EventRemoveInvestigators", {
32
+ display: "false",
33
+ buttons: "0",
34
+ remove: "TokenInvestigators"
35
+ })
36
+ ```
37
+
38
+ ## Event Loops
39
+
40
+ Loops repeat a block of events until an exit condition is met. Structure: **Controller → Body → Exit**.
41
+
42
+ ### Structure
43
+
44
+ 1. **Init Event**: Set loop counter variable to 0
45
+ 2. **Controller Event**: Silent event with vartests checking counter vs limit
46
+ - If counter < limit → route to Body
47
+ - If counter >= limit → route to Exit
48
+ 3. **Body Event(s)**: The repeated content. Increments counter, routes back to Controller.
49
+ 4. **Exit Event**: Continues the scenario after the loop ends.
50
+
51
+ ### Example — 3-iteration loop
52
+
53
+ ```
54
+ # Initialize
55
+ upsert_event("EventLoopInit", {
56
+ display: "false",
57
+ buttons: "1",
58
+ operations: "loopCount,=,0",
59
+ event1: "EventLoopController"
60
+ })
61
+
62
+ # Controller — check if done
63
+ upsert_event("EventLoopController", {
64
+ display: "false",
65
+ buttons: "2",
66
+ vartests: "VarOperation:loopCount,>=,3",
67
+ event1: "EventLoopBody", # button1 = test FAILS (keep looping)
68
+ event2: "EventLoopExit" # button2 = test PASSES (exit)
69
+ })
70
+
71
+ # Body — do work, increment, return to controller
72
+ upsert_event("EventLoopBody", {
73
+ display: "true",
74
+ buttons: "1",
75
+ operations: "loopCount,+,1",
76
+ event1: "EventLoopController"
77
+ })
78
+
79
+ # Exit
80
+ upsert_event("EventLoopExit", {
81
+ display: "true",
82
+ buttons: "1",
83
+ event1: "EventNextScene"
84
+ })
85
+ ```
86
+
87
+ **Important — vartests button mapping:**
88
+ - `button1` / `event1` = test **FAILS** (condition not met)
89
+ - `button2` / `event2` = test **PASSES** (condition met)
90
+
91
+ ### Infinite Loop Avoidance
92
+
93
+ - Always increment the counter in the body
94
+ - Always have an exit path when counter reaches the limit
95
+ - Test with `>=` not `==` to avoid off-by-one infinite loops
96
+
97
+ ## Multi-Question Dialogues
98
+
99
+ A dialogue where each of N questions has its own pass/fail outcome, requiring 2^N permutation events for all combinations. For 3 questions, that's 8 final outcome events.
100
+
101
+ ### 3-Question Template (8 permutations)
102
+
103
+ ```
104
+ # Question 1 — skill test
105
+ upsert_event("EventQ1", {
106
+ buttons: "2",
107
+ quota: "1",
108
+ event1: "EventQ2fromQ1Pass", # Q1 passed → Q2
109
+ event2: "EventQ2fromQ1Fail" # Q1 failed → Q2
110
+ })
111
+
112
+ # Question 2 — after Q1 pass
113
+ upsert_event("EventQ2fromQ1Pass", {
114
+ buttons: "2",
115
+ quota: "1",
116
+ event1: "EventQ3_PP", # Q1 pass, Q2 pass → Q3
117
+ event2: "EventQ3_PF" # Q1 pass, Q2 fail → Q3
118
+ })
119
+
120
+ # Question 2 — after Q1 fail
121
+ upsert_event("EventQ2fromQ1Fail", {
122
+ buttons: "2",
123
+ quota: "1",
124
+ event1: "EventQ3_FP", # Q1 fail, Q2 pass → Q3
125
+ event2: "EventQ3_FF" # Q1 fail, Q2 fail → Q3
126
+ })
127
+
128
+ # Question 3 — four variants (PP, PF, FP, FF from Q1+Q2)
129
+ # Each has 2 outcomes → 8 total final events
130
+ upsert_event("EventQ3_PP", {
131
+ buttons: "2",
132
+ quota: "1",
133
+ event1: "EventOutcome_PPP",
134
+ event2: "EventOutcome_PPF"
135
+ })
136
+ # ... (EventQ3_PF, EventQ3_FP, EventQ3_FF similarly)
137
+
138
+ # 8 Outcome events: PPP, PPF, PFP, PFF, FPP, FPF, FFP, FFF
139
+ # Each shows narrative based on the combination of successes/failures
140
+ ```
141
+
142
+ ### Simplification with Variables
143
+
144
+ For more than 3 questions, use variables instead of permutation events:
145
+ ```
146
+ # Track successes with a variable
147
+ upsert_event("EventQ1", {
148
+ buttons: "2",
149
+ quota: "1",
150
+ operations: "successes,=,0",
151
+ event1: "EventQ1Pass",
152
+ event2: "EventQ1Fail"
153
+ })
154
+
155
+ upsert_event("EventQ1Pass", {
156
+ display: "false",
157
+ buttons: "1",
158
+ operations: "successes,+,1",
159
+ event1: "EventQ2"
160
+ })
161
+ # ... then check successes threshold at the end
162
+ ```
163
+
164
+ ## Token Swap (Silent Update)
165
+
166
+ Replace one token with another in a single event. Used to update the map state (e.g., locked door → open door).
167
+
168
+ ```
169
+ upsert_event("EventUnlockDoor", {
170
+ display: "false",
171
+ buttons: "1",
172
+ remove: "TokenLockedDoor",
173
+ add: "TokenOpenDoor",
174
+ event1: "EventDoorOpened"
175
+ })
176
+ ```
177
+
178
+ **When to use:**
179
+ - Changing token type (explore → interact)
180
+ - Updating visual state (locked → unlocked)
181
+ - Replacing a monster spawn with a different one
182
+ - Any state transition that should be invisible to the player
183
+
184
+ ## Random Event Selection
185
+
186
+ Use `randomevents=true` to randomly pick one event from the event list instead of showing all buttons.
187
+
188
+ ```
189
+ upsert_event("EventRandomEncounter", {
190
+ display: "false",
191
+ buttons: "3",
192
+ randomevents: "true",
193
+ event1: "EventEncounterGhost",
194
+ event2: "EventEncounterCultist",
195
+ event3: "EventEncounterHorror"
196
+ })
197
+ ```
198
+
199
+ ### Preventing Repeats with Flag Variables
200
+
201
+ Use a flag variable per outcome to skip already-seen events:
202
+
203
+ ```
204
+ # Random selector with conditions on sub-events
205
+ upsert_event("EventRandomEncounter", {
206
+ display: "false",
207
+ buttons: "3",
208
+ randomevents: "true",
209
+ event1: "EventEncA",
210
+ event2: "EventEncB",
211
+ event3: "EventEncC"
212
+ })
213
+
214
+ # Each encounter sets a "seen" flag
215
+ upsert_event("EventEncA", {
216
+ buttons: "1",
217
+ conditions: "seenA,==,0", # skip if already seen
218
+ operations: "seenA,=,1",
219
+ event1: "EventContinue"
220
+ })
221
+ ```
222
+
223
+ ## Variable-Controlled Branching
224
+
225
+ Use vartests to route events based on game state. Events are evaluated in button order.
226
+
227
+ ### Ordered vartests (Multiple Thresholds)
228
+
229
+ ```
230
+ upsert_event("EventCheckProgress", {
231
+ display: "false",
232
+ buttons: "2",
233
+ vartests: "VarOperation:cluesFound,>=,3",
234
+ event1: "EventNotEnoughClues", # test FAILS (< 3 clues)
235
+ event2: "EventEnoughClues" # test PASSES (>= 3 clues)
236
+ })
237
+ ```
238
+
239
+ ### conditions vs vartests
240
+
241
+ | Feature | `conditions` | `vartests` |
242
+ |---------|-------------|------------|
243
+ | Check timing | Before event displays | After event displays |
244
+ | If false | Event silently skipped | Routes to button1/event1 |
245
+ | If true | Event proceeds normally | Routes to button2/event2 |
246
+ | Format | `var,comparator,value` (space-separated, AND logic) | `VarOperation:var,comparator,value` |
247
+ | Use case | Gate tokens, spawns, events | Branch event flow |
248
+
249
+ ### Multi-variable Check
250
+
251
+ ```
252
+ upsert_event("EventFinalCheck", {
253
+ display: "false",
254
+ buttons: "2",
255
+ vartests: "VarOperation:key1Found,>=,1 VarOperation:key2Found,>=,1 VarTestsLogicalOperator:AND",
256
+ event1: "EventMissingKeys",
257
+ event2: "EventBothKeysFound"
258
+ })
259
+ ```
260
+
261
+ ## Pattern Naming Conventions
262
+
263
+ Use consistent naming to keep complex event flows readable:
264
+ - `Event<Location><Action>` — e.g., `EventStudyExplore`, `EventHallwayFight`
265
+ - `Event<Name>Pass` / `Event<Name>Fail` — skill test outcomes
266
+ - `Event<Name>Loop` / `Event<Name>LoopBody` / `Event<Name>LoopExit` — loop components
267
+ - `EventQ1`, `EventQ2`, etc. — dialogue questions
268
+ - `0_<Name>` / `1_<Name>` prefixes for loop init/body events within a tile context
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: items-and-distribution
3
+ description: Item distribution patterns for Valkyrie MoM. Use when giving random items, unique items, starting items, or creating item inspection events.
4
+ ---
5
+
6
+ # /items-and-distribution - Item Distribution Patterns
7
+
8
+ Patterns for distributing items to investigators in Mansions of Madness scenarios.
9
+
10
+ ## Item Distribution Modes
11
+
12
+ ### Random Items via Traits
13
+
14
+ Give a random item matching a trait category. Leave `itemname` empty and set `traits`:
15
+
16
+ ```
17
+ upsert_item("QItemSearchWeapon", {
18
+ traits: "weapon"
19
+ })
20
+
21
+ upsert_item("QItemSearchLight", {
22
+ traits: "lightsource"
23
+ })
24
+
25
+ upsert_item("QItemSearchCommon", {
26
+ traits: "common"
27
+ })
28
+ ```
29
+
30
+ Available traits: `weapon`, `lightsource`, `equipment`, `common`, `spell`
31
+
32
+ The game randomly selects an item matching the trait from its internal pool.
33
+
34
+ ### Unique Items
35
+
36
+ Give a specific named item. Set `itemname` and leave `traits` empty:
37
+
38
+ ```
39
+ upsert_item("QItemPuzzleBox", {
40
+ itemname: "ItemCommonPuzzleBox"
41
+ })
42
+
43
+ upsert_item("QItemKnife", {
44
+ itemname: "ItemCommonKnife"
45
+ })
46
+ ```
47
+
48
+ Use `search_game_content` with type "Item" to find valid item catalog IDs.
49
+
50
+ ### Multiple Item Pool
51
+
52
+ Specify multiple items — the game picks one at random:
53
+
54
+ ```
55
+ upsert_item("QItemRandomWeapon", {
56
+ itemname: "ItemCommonKnife ItemCommonAxe ItemCommonPipe"
57
+ })
58
+ ```
59
+
60
+ ### Unique Items with Trait Fallback
61
+
62
+ Combine `itemname` and `traits`. The `itemname` acts as an exclusion list — the game picks a random item matching the trait that is NOT in the `itemname` list:
63
+
64
+ ```
65
+ # Give a random common item, but never the knife or lantern
66
+ upsert_item("QItemMysteryCommon", {
67
+ traits: "common",
68
+ itemname: "ItemCommonKnife ItemCommonKeroseneLantern"
69
+ })
70
+ ```
71
+
72
+ ## Starting Items
73
+
74
+ Items given to investigators at the beginning of the scenario. Set `starting=True`:
75
+
76
+ ```
77
+ # Each investigator gets a random weapon
78
+ upsert_item("QItemStartWeapon", {
79
+ starting: "True",
80
+ traits: "weapon"
81
+ })
82
+
83
+ # Each investigator gets a light source
84
+ upsert_item("QItemStartLight", {
85
+ starting: "True",
86
+ traits: "lightsource"
87
+ })
88
+
89
+ # A specific starting item for the scenario
90
+ upsert_item("QItemStartLetter", {
91
+ starting: "True",
92
+ itemname: "ItemCommonOldJournal"
93
+ })
94
+ ```
95
+
96
+ ### Starting Items with Exclusions
97
+
98
+ Give a random starting item from a trait pool, excluding specific items:
99
+
100
+ ```
101
+ upsert_item("QItemStartEquip", {
102
+ starting: "True",
103
+ traits: "equipment",
104
+ itemname: "ItemCommonBarricade ItemCommonAlarmClock"
105
+ })
106
+ ```
107
+
108
+ ## Item Inspection Events
109
+
110
+ Attach an event to an item that fires when the investigator inspects it. Use the `inspect` field:
111
+
112
+ ```
113
+ upsert_item("QItemMysteriousLetter", {
114
+ itemname: "ItemCommonOldJournal",
115
+ inspect: "EventReadLetter"
116
+ })
117
+
118
+ upsert_event("EventReadLetter", {
119
+ buttons: "1",
120
+ event1: "EventLetterRead"
121
+ })
122
+
123
+ set_localization({
124
+ "EventReadLetter.text": "<i>The faded handwriting reads:\\n\\n\"They must not find the key. I have hidden it where only the moonlight touches.\"</i>",
125
+ "EventReadLetter.button1": "{qst:CONTINUE}"
126
+ })
127
+ ```
128
+
129
+ Inspection events are great for:
130
+ - Revealing clues when players examine found items
131
+ - Adding lore and world-building
132
+ - Triggering state changes (set variables when an item is read)
133
+ - Multi-use items that change over time (use vartests in the inspection event)
134
+
135
+ ## Adding/Removing Items via Events
136
+
137
+ ### Giving Items During Events
138
+
139
+ Use the `add` field on an event to give an item to the active investigator:
140
+
141
+ ```
142
+ upsert_event("EventFindKey", {
143
+ buttons: "1",
144
+ add: "QItemGoldenKey",
145
+ event1: "EventKeyFound"
146
+ })
147
+ ```
148
+
149
+ ### Removing Items During Events
150
+
151
+ Use the `remove` field to take an item away:
152
+
153
+ ```
154
+ upsert_event("EventUseKey", {
155
+ display: "false",
156
+ buttons: "1",
157
+ remove: "QItemGoldenKey",
158
+ add: "TokenUnlockedDoor",
159
+ event1: "EventDoorUnlocked"
160
+ })
161
+ ```
162
+
163
+ ### Conditional Item Checks
164
+
165
+ Check if a player has found an item by using a tracking variable:
166
+
167
+ ```
168
+ # When finding the key, set a flag
169
+ upsert_event("EventFindKey", {
170
+ buttons: "1",
171
+ operations: "hasKey,=,1",
172
+ add: "QItemGoldenKey",
173
+ event1: "EventKeyNarrative"
174
+ })
175
+
176
+ # Later, check the flag
177
+ upsert_event("EventTryDoor", {
178
+ display: "false",
179
+ buttons: "2",
180
+ vartests: "VarOperation:hasKey,>=,1",
181
+ event1: "EventDoorLocked", # no key
182
+ event2: "EventDoorUnlock" # has key
183
+ })
184
+ ```
185
+
186
+ ## Distribution Strategy Tips
187
+
188
+ 1. **Balance**: Aim for 1 search token per tile area, giving roughly 1 item per area explored
189
+ 2. **Scaling**: For larger parties (4-5 heroes), add extra search tokens or starting items
190
+ 3. **Pacing**: Place weapon searches near combat areas, light sources near dark areas
191
+ 4. **Specialization**: Use specific items (`itemname`) for story-critical gear; use traits for general loot
192
+ 5. **Economy**: Don't over-distribute — scarcity creates tension. 6-8 total items for a medium scenario
193
+ 6. **Starting loadout**: 1 weapon + 1 utility item per hero is a good baseline