@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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/validation/rules/ui-lifetime.d.ts +16 -0
- package/dist/validation/rules/ui-lifetime.d.ts.map +1 -0
- package/dist/validation/rules/ui-lifetime.js +105 -0
- package/dist/validation/rules/ui-lifetime.js.map +1 -0
- package/dist/validation/validator.d.ts.map +1 -1
- package/dist/validation/validator.js +2 -0
- package/dist/validation/validator.js.map +1 -1
- package/package.json +1 -1
- package/skills/artwork/SKILL.md +1 -0
- package/skills/ui-and-puzzles/SKILL.md +4 -0
- package/src/validation/rules/ui-lifetime.ts +99 -0
- package/src/validation/validator.ts +2 -0
|
@@ -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;
|
|
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;
|
|
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
package/skills/artwork/SKILL.md
CHANGED
|
@@ -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
|
/**
|