@thijshakkenberg/valkyrie-mom-mcp 2.3.2 → 2.3.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valkyrie-mom",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "AI-assisted Mansions of Madness scenario editor for Valkyrie",
5
5
  "author": {
6
6
  "name": "Thijs Hakkenberg"
@@ -0,0 +1,16 @@
1
+ import type { ScenarioModel } from '../../model/scenario-model.js';
2
+ import type { ValidationResult } from '../../model/component-types.js';
3
+ /**
4
+ * UI must be off the board when a chain of events ends. While any UI component is on the
5
+ * board, Valkyrie's "next phase" button does nothing (NextStageButton.Next returns when
6
+ * Quest.UIItemsPresent()), so a UI left behind — a status panel, a portrait, a handout nobody
7
+ * removes — stops the investigator phase from ever ending: no mythos, no monsters.
8
+ *
9
+ * Follows every chain from what can start one (triggered events, token clicks, item
10
+ * inspections, monster events) and tracks which UI is on the board. A chain may end with UI
11
+ * showing only when the scenario has ended, or when that UI is clickable and clicking it leads
12
+ * to its removal (a Begin button). A clickable UI whose chain comes back with it still showing
13
+ * (a status panel that re-adds itself) never leaves.
14
+ */
15
+ export declare function checkUILifetime(model: ScenarioModel): ValidationResult[];
16
+ //# sourceMappingURL=ui-lifetime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-lifetime.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/ui-lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAQvE;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CA6ExE"}
@@ -0,0 +1,105 @@
1
+ const UI_REMOVE_KEYWORDS = new Set(['#uicomponents', '#boardcomponents']);
2
+ /** Stop exploring huge scenarios after this many (event, UI on board) states */
3
+ const MAX_STATES = 50_000;
4
+ const refs = (v) => (v ?? '').split(/\s+/).filter(Boolean);
5
+ /**
6
+ * UI must be off the board when a chain of events ends. While any UI component is on the
7
+ * board, Valkyrie's "next phase" button does nothing (NextStageButton.Next returns when
8
+ * Quest.UIItemsPresent()), so a UI left behind — a status panel, a portrait, a handout nobody
9
+ * removes — stops the investigator phase from ever ending: no mythos, no monsters.
10
+ *
11
+ * Follows every chain from what can start one (triggered events, token clicks, item
12
+ * inspections, monster events) and tracks which UI is on the board. A chain may end with UI
13
+ * showing only when the scenario has ended, or when that UI is clickable and clicking it leads
14
+ * to its removal (a Begin button). A clickable UI whose chain comes back with it still showing
15
+ * (a status panel that re-adds itself) never leaves.
16
+ */
17
+ export function checkUILifetime(model) {
18
+ const next = (name) => {
19
+ const d = model.get(name)?.data;
20
+ if (!d)
21
+ return [];
22
+ const buttons = parseInt(d.buttons ?? '0', 10) || 0;
23
+ // A hidden event always follows button 1
24
+ const count = d.display === 'false' ? Math.min(buttons, 1) : buttons;
25
+ const out = [];
26
+ for (let i = 1; i <= count; i++)
27
+ out.push(...refs(d[`event${i}`]));
28
+ return out.filter(n => model.get(n));
29
+ };
30
+ const isClickableUI = (ui) => {
31
+ const d = model.get(ui)?.data;
32
+ return !!d && (parseInt(d.buttons ?? '0', 10) || 0) >= 1 && refs(d.event1).length > 0;
33
+ };
34
+ // Where chains start
35
+ const entries = new Set();
36
+ for (const comp of model.getAll()) {
37
+ const d = comp.data;
38
+ if (d.trigger)
39
+ entries.add(comp.name);
40
+ if (comp.name.startsWith('Token'))
41
+ entries.add(comp.name);
42
+ if (comp.name.startsWith('QItem'))
43
+ refs(d.inspect).forEach(e => entries.add(e));
44
+ if (comp.name.startsWith('CustomMonster'))
45
+ [d.activation, d.evadeevent, d.horrorevent].flatMap(refs).forEach(e => entries.add(e));
46
+ }
47
+ // UI name -> the event after which it was still showing, and where that chain began
48
+ const leftOver = new Map();
49
+ const seen = new Set();
50
+ let states = 0;
51
+ const visit = (name, onBoard, from, clicked) => {
52
+ const key = `${name}|${[...onBoard].sort().join(',')}|${clicked ?? ''}`;
53
+ if (seen.has(key)) {
54
+ // Back where we were, and the UI whose click led here is still showing: it never goes away
55
+ if (clicked && onBoard.has(clicked) && !leftOver.has(clicked))
56
+ leftOver.set(clicked, { at: name, from });
57
+ return;
58
+ }
59
+ if (++states > MAX_STATES)
60
+ return;
61
+ seen.add(key);
62
+ const d = model.get(name)?.data ?? {};
63
+ const board = new Set(onBoard);
64
+ // Valkyrie adds first, then removes
65
+ refs(d.add).filter(n => n.startsWith('UI')).forEach(n => board.add(n));
66
+ for (const r of refs(d.remove)) {
67
+ if (UI_REMOVE_KEYWORDS.has(r))
68
+ board.clear();
69
+ else
70
+ board.delete(r);
71
+ }
72
+ if (/(^|\s)\$end,=,[1-9]/.test(d.operations ?? ''))
73
+ return;
74
+ const targets = next(name);
75
+ if (targets.length > 0) {
76
+ for (const t of targets)
77
+ visit(t, board, from, clicked);
78
+ return;
79
+ }
80
+ // The chain ends here: a clickable UI on the board carries the story on
81
+ const clickable = [...board].filter(isClickableUI);
82
+ if (clickable.length > 0) {
83
+ for (const ui of clickable)
84
+ for (const t of refs(model.get(ui).data.event1))
85
+ visit(t, board, from, ui);
86
+ return;
87
+ }
88
+ for (const ui of board)
89
+ if (!leftOver.has(ui))
90
+ leftOver.set(ui, { at: name, from });
91
+ };
92
+ for (const e of entries)
93
+ visit(e, new Set(), e);
94
+ const results = [];
95
+ for (const [ui, { at, from }] of leftOver) {
96
+ results.push({
97
+ rule: 'ui-lifetime',
98
+ severity: 'warning',
99
+ message: `"${ui}" is still on the board when "${at}" ends (a chain that starts at "${from}"). While any UI element is on the board, Valkyrie's next phase button does nothing, so the investigator phase never ends. Remove it on every button that leaves it; use a token with customImage for anything that should stay on the board`,
100
+ component: ui,
101
+ });
102
+ }
103
+ return results;
104
+ }
105
+ //# sourceMappingURL=ui-lifetime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-lifetime.js","sourceRoot":"","sources":["../../../src/validation/rules/ui-lifetime.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AA4ChE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAMzE"}
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AA8ChE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAMzE"}
@@ -17,6 +17,7 @@ import { checkTriggers } from './rules/triggers.js';
17
17
  import { checkGameFlow } from './rules/game-flow.js';
18
18
  import { checkPuzzles } from './rules/puzzles.js';
19
19
  import { checkPackageName } from './rules/package-name.js';
20
+ import { checkUILifetime } from './rules/ui-lifetime.js';
20
21
  const ALL_RULES = [
21
22
  checkRequiredFields,
22
23
  checkCrossReferences,
@@ -38,6 +39,7 @@ const ALL_RULES = [
38
39
  checkGameFlow,
39
40
  checkPuzzles,
40
41
  checkPackageName,
42
+ checkUILifetime,
41
43
  ];
42
44
  /**
43
45
  * Runs all validation rules against a scenario model and aggregates results.
@@ -1 +1 @@
1
- {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,6BAA6B,EAAE,MAAM,uCAAuC,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,MAAM,SAAS,GAAG;IAChB,mBAAmB;IACnB,oBAAoB;IACpB,eAAe;IACf,6BAA6B;IAC7B,gBAAgB;IAChB,sBAAsB;IACtB,oBAAoB;IACpB,wBAAwB;IACxB,6BAA6B;IAC7B,qBAAqB;IACrB,mBAAmB;IACnB,cAAc;IACd,qBAAqB;IACrB,gBAAgB;IAChB,mBAAmB;IACnB,iBAAiB;IACjB,aAAa;IACb,aAAa;IACb,YAAY;IACZ,gBAAgB;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,6BAA6B,EAAE,MAAM,uCAAuC,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,SAAS,GAAG;IAChB,mBAAmB;IACnB,oBAAoB;IACpB,eAAe;IACf,6BAA6B;IAC7B,gBAAgB;IAChB,sBAAsB;IACtB,oBAAoB;IACpB,wBAAwB;IACxB,6BAA6B;IAC7B,qBAAqB;IACrB,mBAAmB;IACnB,cAAc;IACd,qBAAqB;IACrB,gBAAgB;IAChB,mBAAmB;IACnB,iBAAiB;IACjB,aAAa;IACb,aAAa;IACb,YAAY;IACZ,gBAAgB;IAChB,eAAe;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thijshakkenberg/valkyrie-mom-mcp",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "MCP server for AI-assisted Mansions of Madness scenario creation with Valkyrie",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -102,6 +102,7 @@ upsert_event("EventReadLetter", { buttons: "1", event1: "EventHideLetter", add:
102
102
  upsert_event("EventHideLetter", { display: "false", buttons: "0", remove: "UILetter" })
103
103
  ```
104
104
 
105
+ - Never leave a picture up: while any UI element is on the board, the next phase button does nothing. For a portrait that stays available, use a token with `customImage`.
105
106
  - Use `size` 0.6–0.7 for portrait handouts and 0.55–0.65 for landscape scenes, so a long dialog text still clears the picture.
106
107
  - For an item the players keep, give the item `inspect=EventReadLetter`, so the picture comes back whenever they inspect it.
107
108
 
@@ -39,6 +39,10 @@ add: "UIBackground UIButton UIImage UIText"
39
39
 
40
40
  A UI element with `buttons=1` and `event1=` is itself clickable, and its label is `<name>.uitext`.
41
41
 
42
+ ### UI never stays on the board
43
+
44
+ While **any** UI element is on the board, Valkyrie's next phase button does nothing (`Quest.UIItemsPresent()`), so the investigator phase can never end: no mythos, no monsters. UI is for cutscenes, handouts and puzzles that the next button removes again. For something that should stay available all game (a companion, a status sheet), use a token with `customImage`, and show the values in its dialog with `{var:Name}`, which dialog text fills in each time it opens. `validate_scenario` warns about UI that nothing removes (rule `ui-lifetime`).
45
+
42
46
  ## Prologue / Intro Cutscene
43
47
 
44
48
  `/artwork` has the complete, tested pattern. It uses a full-screen `ImageCutsceneBG` backdrop, the scenario's own picture on the left (`xposition=-0.45`), the story on the right (`xposition=0.35`, `textAlignment=TOP`), and a Begin button anchored to the bottom (`valign=bottom`) whose event removes the UI and sets up the board. `/artwork` also covers showing handouts and scene pictures during play, and generating the pictures with ComfyUI.
@@ -0,0 +1,99 @@
1
+ import type { ScenarioModel } from '../../model/scenario-model.js';
2
+ import type { ValidationResult } from '../../model/component-types.js';
3
+
4
+ const UI_REMOVE_KEYWORDS = new Set(['#uicomponents', '#boardcomponents']);
5
+ /** Stop exploring huge scenarios after this many (event, UI on board) states */
6
+ const MAX_STATES = 50_000;
7
+
8
+ const refs = (v: string | undefined) => (v ?? '').split(/\s+/).filter(Boolean);
9
+
10
+ /**
11
+ * UI must be off the board when a chain of events ends. While any UI component is on the
12
+ * board, Valkyrie's "next phase" button does nothing (NextStageButton.Next returns when
13
+ * Quest.UIItemsPresent()), so a UI left behind — a status panel, a portrait, a handout nobody
14
+ * removes — stops the investigator phase from ever ending: no mythos, no monsters.
15
+ *
16
+ * Follows every chain from what can start one (triggered events, token clicks, item
17
+ * inspections, monster events) and tracks which UI is on the board. A chain may end with UI
18
+ * showing only when the scenario has ended, or when that UI is clickable and clicking it leads
19
+ * to its removal (a Begin button). A clickable UI whose chain comes back with it still showing
20
+ * (a status panel that re-adds itself) never leaves.
21
+ */
22
+ export function checkUILifetime(model: ScenarioModel): ValidationResult[] {
23
+ const next = (name: string): string[] => {
24
+ const d = model.get(name)?.data;
25
+ if (!d) return [];
26
+ const buttons = parseInt(d.buttons ?? '0', 10) || 0;
27
+ // A hidden event always follows button 1
28
+ const count = d.display === 'false' ? Math.min(buttons, 1) : buttons;
29
+ const out: string[] = [];
30
+ for (let i = 1; i <= count; i++) out.push(...refs(d[`event${i}`]));
31
+ return out.filter(n => model.get(n));
32
+ };
33
+ const isClickableUI = (ui: string) => {
34
+ const d = model.get(ui)?.data;
35
+ return !!d && (parseInt(d.buttons ?? '0', 10) || 0) >= 1 && refs(d.event1).length > 0;
36
+ };
37
+
38
+ // Where chains start
39
+ const entries = new Set<string>();
40
+ for (const comp of model.getAll()) {
41
+ const d = comp.data;
42
+ if (d.trigger) entries.add(comp.name);
43
+ if (comp.name.startsWith('Token')) entries.add(comp.name);
44
+ if (comp.name.startsWith('QItem')) refs(d.inspect).forEach(e => entries.add(e));
45
+ if (comp.name.startsWith('CustomMonster')) [d.activation, d.evadeevent, d.horrorevent].flatMap(refs).forEach(e => entries.add(e));
46
+ }
47
+
48
+ // UI name -> the event after which it was still showing, and where that chain began
49
+ const leftOver = new Map<string, { at: string; from: string }>();
50
+ const seen = new Set<string>();
51
+ let states = 0;
52
+
53
+ const visit = (name: string, onBoard: Set<string>, from: string, clicked?: string) => {
54
+ const key = `${name}|${[...onBoard].sort().join(',')}|${clicked ?? ''}`;
55
+ if (seen.has(key)) {
56
+ // Back where we were, and the UI whose click led here is still showing: it never goes away
57
+ if (clicked && onBoard.has(clicked) && !leftOver.has(clicked)) leftOver.set(clicked, { at: name, from });
58
+ return;
59
+ }
60
+ if (++states > MAX_STATES) return;
61
+ seen.add(key);
62
+
63
+ const d = model.get(name)?.data ?? {};
64
+ const board = new Set(onBoard);
65
+ // Valkyrie adds first, then removes
66
+ refs(d.add).filter(n => n.startsWith('UI')).forEach(n => board.add(n));
67
+ for (const r of refs(d.remove)) {
68
+ if (UI_REMOVE_KEYWORDS.has(r)) board.clear();
69
+ else board.delete(r);
70
+ }
71
+ if (/(^|\s)\$end,=,[1-9]/.test(d.operations ?? '')) return;
72
+
73
+ const targets = next(name);
74
+ if (targets.length > 0) {
75
+ for (const t of targets) visit(t, board, from, clicked);
76
+ return;
77
+ }
78
+ // The chain ends here: a clickable UI on the board carries the story on
79
+ const clickable = [...board].filter(isClickableUI);
80
+ if (clickable.length > 0) {
81
+ for (const ui of clickable) for (const t of refs(model.get(ui)!.data.event1)) visit(t, board, from, ui);
82
+ return;
83
+ }
84
+ for (const ui of board) if (!leftOver.has(ui)) leftOver.set(ui, { at: name, from });
85
+ };
86
+
87
+ for (const e of entries) visit(e, new Set(), e);
88
+
89
+ const results: ValidationResult[] = [];
90
+ for (const [ui, { at, from }] of leftOver) {
91
+ results.push({
92
+ rule: 'ui-lifetime',
93
+ severity: 'warning',
94
+ message: `"${ui}" is still on the board when "${at}" ends (a chain that starts at "${from}"). While any UI element is on the board, Valkyrie's next phase button does nothing, so the investigator phase never ends. Remove it on every button that leaves it; use a token with customImage for anything that should stay on the board`,
95
+ component: ui,
96
+ });
97
+ }
98
+ return results;
99
+ }
@@ -19,6 +19,7 @@ import { checkTriggers } from './rules/triggers.js';
19
19
  import { checkGameFlow } from './rules/game-flow.js';
20
20
  import { checkPuzzles } from './rules/puzzles.js';
21
21
  import { checkPackageName } from './rules/package-name.js';
22
+ import { checkUILifetime } from './rules/ui-lifetime.js';
22
23
 
23
24
  const ALL_RULES = [
24
25
  checkRequiredFields,
@@ -41,6 +42,7 @@ const ALL_RULES = [
41
42
  checkGameFlow,
42
43
  checkPuzzles,
43
44
  checkPackageName,
45
+ checkUILifetime,
44
46
  ];
45
47
 
46
48
  /**