@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.
- package/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +9 -0
- package/CLAUDE.md +73 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/agents/scenario-designer.md +114 -0
- package/dist/catalogs/catalog-store.d.ts +20 -0
- package/dist/catalogs/catalog-store.d.ts.map +1 -0
- package/dist/catalogs/catalog-store.js +78 -0
- package/dist/catalogs/catalog-store.js.map +1 -0
- package/dist/catalogs/catalog-types.d.ts +10 -0
- package/dist/catalogs/catalog-types.d.ts.map +1 -0
- package/dist/catalogs/catalog-types.js +2 -0
- package/dist/catalogs/catalog-types.js.map +1 -0
- package/dist/catalogs/data/all-catalogs.d.ts +9 -0
- package/dist/catalogs/data/all-catalogs.d.ts.map +1 -0
- package/dist/catalogs/data/all-catalogs.js +8383 -0
- package/dist/catalogs/data/all-catalogs.js.map +1 -0
- package/dist/catalogs/data/tile-geometry.d.ts +12 -0
- package/dist/catalogs/data/tile-geometry.d.ts.map +1 -0
- package/dist/catalogs/data/tile-geometry.js +183 -0
- package/dist/catalogs/data/tile-geometry.js.map +1 -0
- package/dist/catalogs/extract-catalogs.d.ts +12 -0
- package/dist/catalogs/extract-catalogs.d.ts.map +1 -0
- package/dist/catalogs/extract-catalogs.js +128 -0
- package/dist/catalogs/extract-catalogs.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/io/ini-parser.d.ts +6 -0
- package/dist/io/ini-parser.d.ts.map +1 -0
- package/dist/io/ini-parser.js +42 -0
- package/dist/io/ini-parser.js.map +1 -0
- package/dist/io/ini-writer.d.ts +2 -0
- package/dist/io/ini-writer.d.ts.map +1 -0
- package/dist/io/ini-writer.js +36 -0
- package/dist/io/ini-writer.js.map +1 -0
- package/dist/io/localization-io.d.ts +21 -0
- package/dist/io/localization-io.d.ts.map +1 -0
- package/dist/io/localization-io.js +51 -0
- package/dist/io/localization-io.js.map +1 -0
- package/dist/io/package-builder.d.ts +2 -0
- package/dist/io/package-builder.d.ts.map +1 -0
- package/dist/io/package-builder.js +22 -0
- package/dist/io/package-builder.js.map +1 -0
- package/dist/model/component-types.d.ts +61 -0
- package/dist/model/component-types.d.ts.map +1 -0
- package/dist/model/component-types.js +71 -0
- package/dist/model/component-types.js.map +1 -0
- package/dist/model/localization-store.d.ts +15 -0
- package/dist/model/localization-store.d.ts.map +1 -0
- package/dist/model/localization-store.js +42 -0
- package/dist/model/localization-store.js.map +1 -0
- package/dist/model/scenario-model.d.ts +27 -0
- package/dist/model/scenario-model.d.ts.map +1 -0
- package/dist/model/scenario-model.js +151 -0
- package/dist/model/scenario-model.js.map +1 -0
- package/dist/resources/format-docs.d.ts +6 -0
- package/dist/resources/format-docs.d.ts.map +1 -0
- package/dist/resources/format-docs.js +261 -0
- package/dist/resources/format-docs.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +206 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/lifecycle.d.ts +37 -0
- package/dist/tools/lifecycle.d.ts.map +1 -0
- package/dist/tools/lifecycle.js +146 -0
- package/dist/tools/lifecycle.js.map +1 -0
- package/dist/tools/map.d.ts +14 -0
- package/dist/tools/map.d.ts.map +1 -0
- package/dist/tools/map.js +98 -0
- package/dist/tools/map.js.map +1 -0
- package/dist/tools/reference.d.ts +4 -0
- package/dist/tools/reference.d.ts.map +1 -0
- package/dist/tools/reference.js +6 -0
- package/dist/tools/reference.js.map +1 -0
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +22 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/upsert.d.ts +15 -0
- package/dist/tools/upsert.d.ts.map +1 -0
- package/dist/tools/upsert.js +91 -0
- package/dist/tools/upsert.js.map +1 -0
- package/dist/validation/rules/catalog-references.d.ts +14 -0
- package/dist/validation/rules/catalog-references.d.ts.map +1 -0
- package/dist/validation/rules/catalog-references.js +90 -0
- package/dist/validation/rules/catalog-references.js.map +1 -0
- package/dist/validation/rules/cross-references.d.ts +9 -0
- package/dist/validation/rules/cross-references.d.ts.map +1 -0
- package/dist/validation/rules/cross-references.js +37 -0
- package/dist/validation/rules/cross-references.js.map +1 -0
- package/dist/validation/rules/event-chain-utils.d.ts +8 -0
- package/dist/validation/rules/event-chain-utils.d.ts.map +1 -0
- package/dist/validation/rules/event-chain-utils.js +30 -0
- package/dist/validation/rules/event-chain-utils.js.map +1 -0
- package/dist/validation/rules/event-flow.d.ts +14 -0
- package/dist/validation/rules/event-flow.d.ts.map +1 -0
- package/dist/validation/rules/event-flow.js +83 -0
- package/dist/validation/rules/event-flow.js.map +1 -0
- package/dist/validation/rules/event-graph.d.ts +10 -0
- package/dist/validation/rules/event-graph.d.ts.map +1 -0
- package/dist/validation/rules/event-graph.js +51 -0
- package/dist/validation/rules/event-graph.js.map +1 -0
- package/dist/validation/rules/explore-token-pattern.d.ts +10 -0
- package/dist/validation/rules/explore-token-pattern.d.ts.map +1 -0
- package/dist/validation/rules/explore-token-pattern.js +70 -0
- package/dist/validation/rules/explore-token-pattern.js.map +1 -0
- package/dist/validation/rules/format-rules.d.ts +10 -0
- package/dist/validation/rules/format-rules.d.ts.map +1 -0
- package/dist/validation/rules/format-rules.js +61 -0
- package/dist/validation/rules/format-rules.js.map +1 -0
- package/dist/validation/rules/investigator-token-pattern.d.ts +9 -0
- package/dist/validation/rules/investigator-token-pattern.d.ts.map +1 -0
- package/dist/validation/rules/investigator-token-pattern.js +39 -0
- package/dist/validation/rules/investigator-token-pattern.js.map +1 -0
- package/dist/validation/rules/localization-completeness.d.ts +11 -0
- package/dist/validation/rules/localization-completeness.d.ts.map +1 -0
- package/dist/validation/rules/localization-completeness.js +83 -0
- package/dist/validation/rules/localization-completeness.js.map +1 -0
- package/dist/validation/rules/mythos-structure.d.ts +10 -0
- package/dist/validation/rules/mythos-structure.d.ts.map +1 -0
- package/dist/validation/rules/mythos-structure.js +57 -0
- package/dist/validation/rules/mythos-structure.js.map +1 -0
- package/dist/validation/rules/required-fields.d.ts +9 -0
- package/dist/validation/rules/required-fields.d.ts.map +1 -0
- package/dist/validation/rules/required-fields.js +38 -0
- package/dist/validation/rules/required-fields.js.map +1 -0
- package/dist/validation/rules/tile-connectivity.d.ts +9 -0
- package/dist/validation/rules/tile-connectivity.d.ts.map +1 -0
- package/dist/validation/rules/tile-connectivity.js +125 -0
- package/dist/validation/rules/tile-connectivity.js.map +1 -0
- package/dist/validation/validator.d.ts +7 -0
- package/dist/validation/validator.d.ts.map +1 -0
- package/dist/validation/validator.js +35 -0
- package/dist/validation/validator.js.map +1 -0
- package/package.json +59 -0
- package/skills/custom-monsters/SKILL.md +228 -0
- package/skills/event-patterns/SKILL.md +268 -0
- package/skills/items-and-distribution/SKILL.md +193 -0
- package/skills/scenario/SKILL.md +105 -0
- package/skills/tile-placement/SKILL.md +163 -0
- package/skills/ui-and-puzzles/SKILL.md +347 -0
- package/skills/variables-and-mythos/SKILL.md +285 -0
- package/src/catalogs/catalog-store.ts +94 -0
- package/src/catalogs/catalog-types.ts +10 -0
- package/src/catalogs/data/all-catalogs.ts +8391 -0
- package/src/catalogs/data/tile-geometry.ts +188 -0
- package/src/catalogs/extract-catalogs.ts +172 -0
- package/src/index.ts +14 -0
- package/src/io/ini-parser.ts +52 -0
- package/src/io/ini-writer.ts +51 -0
- package/src/io/localization-io.ts +62 -0
- package/src/io/package-builder.ts +26 -0
- package/src/model/component-types.ts +124 -0
- package/src/model/localization-store.ts +52 -0
- package/src/model/scenario-model.ts +177 -0
- package/src/resources/format-docs.ts +264 -0
- package/src/server.ts +336 -0
- package/src/tools/lifecycle.ts +183 -0
- package/src/tools/map.ts +125 -0
- package/src/tools/reference.ts +7 -0
- package/src/tools/shared.ts +34 -0
- package/src/tools/upsert.ts +133 -0
- package/src/validation/rules/catalog-references.ts +95 -0
- package/src/validation/rules/cross-references.ts +42 -0
- package/src/validation/rules/event-chain-utils.ts +31 -0
- package/src/validation/rules/event-flow.ts +93 -0
- package/src/validation/rules/event-graph.ts +62 -0
- package/src/validation/rules/explore-token-pattern.ts +77 -0
- package/src/validation/rules/format-rules.ts +69 -0
- package/src/validation/rules/investigator-token-pattern.ts +47 -0
- package/src/validation/rules/localization-completeness.ts +93 -0
- package/src/validation/rules/mythos-structure.ts +64 -0
- package/src/validation/rules/required-fields.ts +43 -0
- package/src/validation/rules/tile-connectivity.ts +148 -0
- package/src/validation/validator.ts +38 -0
- 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
|