@patterkit/core 0.9.6 → 0.10.1
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/dist/index.cjs +82 -0
- package/dist/index.d.cts +42 -3
- package/dist/index.d.ts +42 -3
- package/dist/index.js +81 -0
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
hash4: () => hash4,
|
|
28
28
|
humanizeNodeRefs: () => humanizeNodeRefs,
|
|
29
29
|
isValidGameId: () => import_model5.isValidGameId,
|
|
30
|
+
neverRunNodes: () => neverRunNodes,
|
|
30
31
|
newId: () => import_toolkit.newId,
|
|
31
32
|
parseSource: () => import_toolkit3.parseSource,
|
|
32
33
|
planProject: () => planProject,
|
|
@@ -97,6 +98,49 @@ var import_model = require("@patterkit/model");
|
|
|
97
98
|
var import_model2 = require("@patterkit/model");
|
|
98
99
|
var import_model3 = require("@patterkit/model");
|
|
99
100
|
var import_expr = require("@wildwinter/expr");
|
|
101
|
+
|
|
102
|
+
// src/never-runs.ts
|
|
103
|
+
var isNode = (n) => !!n && typeof n === "object" && typeof n.id === "string";
|
|
104
|
+
function neverRunNodes(children) {
|
|
105
|
+
const out = [];
|
|
106
|
+
walkContainer(children, "run", out);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function walkContainer(children, selector, out) {
|
|
110
|
+
if (!Array.isArray(children)) return void 0;
|
|
111
|
+
if (selector === "run") {
|
|
112
|
+
let jumper;
|
|
113
|
+
for (const n of children) {
|
|
114
|
+
if (!isNode(n)) continue;
|
|
115
|
+
if (jumper !== void 0) {
|
|
116
|
+
out.push({ id: n.id, type: n.type, reason: "after-jump", ...jumper });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (n.type === "snippet") {
|
|
120
|
+
if (!n.condition && n.jump && typeof n.jump.to === "string" && n.jump.mode !== "call") jumper = { jumper: n.id, to: n.jump.to };
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const inner = walkContainer(n.children, n.selector ?? "run", out);
|
|
124
|
+
if (inner !== void 0 && !n.condition && !n.prompt && (n.selector ?? "run") === "run") jumper = inner;
|
|
125
|
+
}
|
|
126
|
+
return jumper;
|
|
127
|
+
}
|
|
128
|
+
let winner;
|
|
129
|
+
for (const n of children) {
|
|
130
|
+
if (!isNode(n)) continue;
|
|
131
|
+
if (selector === "branch") {
|
|
132
|
+
if (winner !== void 0) {
|
|
133
|
+
out.push({ id: n.id, type: n.type, reason: "branch-shadowed", by: winner });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!n.condition) winner = n.id;
|
|
137
|
+
}
|
|
138
|
+
if (n.type === "group") walkContainer(n.children, n.selector ?? "run", out);
|
|
139
|
+
}
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/validate.ts
|
|
100
144
|
function choiceLoopsBack(choice, ...targets) {
|
|
101
145
|
const back = new Set(targets.filter((s) => typeof s === "string" && s.length > 0));
|
|
102
146
|
const hits = (n) => n.type === "snippet" ? !!n.jump && back.has(n.jump.to) : (n.children ?? []).some(hits);
|
|
@@ -306,6 +350,17 @@ function validateProject(input) {
|
|
|
306
350
|
} else {
|
|
307
351
|
reportEmpty(block.children);
|
|
308
352
|
}
|
|
353
|
+
for (const n of neverRunNodes(block.children)) {
|
|
354
|
+
if (n.reason !== "after-jump") continue;
|
|
355
|
+
const where2 = n.to === "END" ? "to END" : `to '${n.to}'`;
|
|
356
|
+
issues.push({
|
|
357
|
+
code: "unreachable-after-jump",
|
|
358
|
+
severity: "warning",
|
|
359
|
+
id: n.id,
|
|
360
|
+
jumper: n.jumper,
|
|
361
|
+
message: `${n.type} '${n.id}' in block '${block.id}' never runs: snippet '${n.jumper}' before it always jumps ${where2}`
|
|
362
|
+
});
|
|
363
|
+
}
|
|
309
364
|
(0, import_model.walkNodes)(block.children, (node) => {
|
|
310
365
|
const inBlock = `block '${block.id}'`;
|
|
311
366
|
checkTags(node.tags, `${node.type} '${node.id}'`, node.id);
|
|
@@ -404,6 +459,32 @@ function validateProject(input) {
|
|
|
404
459
|
});
|
|
405
460
|
}
|
|
406
461
|
}
|
|
462
|
+
const wellFormed = scenes.filter((sc) => typeof sc?.id === "string" && Array.isArray(sc.blocks));
|
|
463
|
+
if (wellFormed.length > 0) {
|
|
464
|
+
const start = project.start;
|
|
465
|
+
const scene = start ? wellFormed.find((sc) => sc.id === start.scene) : void 0;
|
|
466
|
+
if (!start) {
|
|
467
|
+
issues.push({
|
|
468
|
+
code: "no-start",
|
|
469
|
+
severity: "warning",
|
|
470
|
+
message: "no start scene is set, so a game that doesn't name a scene starts at the first one"
|
|
471
|
+
});
|
|
472
|
+
} else if (!scene) {
|
|
473
|
+
issues.push({
|
|
474
|
+
code: "dangling-start",
|
|
475
|
+
severity: "warning",
|
|
476
|
+
id: start.scene,
|
|
477
|
+
message: `the start scene '${start.scene}' isn't in the project, so a game that doesn't name a scene starts at the first one`
|
|
478
|
+
});
|
|
479
|
+
} else if (start.block !== void 0 && !scene.blocks.some((b) => b?.id === start.block)) {
|
|
480
|
+
issues.push({
|
|
481
|
+
code: "dangling-start",
|
|
482
|
+
severity: "warning",
|
|
483
|
+
id: start.block,
|
|
484
|
+
message: `the start block '${start.block}' isn't in scene '${scene.id}', so the game starts at that scene's first block`
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
407
488
|
return issues;
|
|
408
489
|
}
|
|
409
490
|
function isQualifierEntry(q) {
|
|
@@ -776,6 +857,7 @@ function planScene(target, scaffold) {
|
|
|
776
857
|
hash4,
|
|
777
858
|
humanizeNodeRefs,
|
|
778
859
|
isValidGameId,
|
|
860
|
+
neverRunNodes,
|
|
779
861
|
newId,
|
|
780
862
|
parseSource,
|
|
781
863
|
planProject,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { newId, parseSource } from '@wildwinter/toolkit';
|
|
2
|
-
import { ProjectFile, Scene, AuthoringFile } from '@patterkit/model';
|
|
2
|
+
import { ProjectFile, Scene, AuthoringFile, Selector } from '@patterkit/model';
|
|
3
3
|
export { effectiveGameId, gameIdify, isValidGameId } from '@patterkit/model';
|
|
4
4
|
|
|
5
5
|
/** Slugify an author name into a filename- / address-safe segment. */
|
|
@@ -26,7 +26,7 @@ interface StringifyOptions {
|
|
|
26
26
|
declare function canonicalStringify(value: unknown, opts?: StringifyOptions): string;
|
|
27
27
|
|
|
28
28
|
interface ValidationIssue {
|
|
29
|
-
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-speaker-label" | "unused-speaker-label" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "invalid-gamedata-value" | "jump-cycle";
|
|
29
|
+
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-speaker-label" | "unused-speaker-label" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "invalid-gamedata-value" | "jump-cycle" | "unreachable-after-jump" | "no-start" | "dangling-start";
|
|
30
30
|
message: string;
|
|
31
31
|
/** Id of the offending node/beat, where applicable. */
|
|
32
32
|
id?: string;
|
|
@@ -40,6 +40,8 @@ interface ValidationIssue {
|
|
|
40
40
|
bad: string;
|
|
41
41
|
options: string[];
|
|
42
42
|
};
|
|
43
|
+
/** `unreachable-after-jump`: the snippet whose jump means this node never runs (`id` is the node). */
|
|
44
|
+
jumper?: string;
|
|
43
45
|
}
|
|
44
46
|
interface ProjectInput {
|
|
45
47
|
project: ProjectFile;
|
|
@@ -50,6 +52,43 @@ interface ProjectInput {
|
|
|
50
52
|
/** Validate a project's structural invariants. Returns an empty array when valid. */
|
|
51
53
|
declare function validateProject(input: ProjectInput): ValidationIssue[];
|
|
52
54
|
|
|
55
|
+
/** The shape the walk reads: an authored node (condition as source text) or a compiled one (an
|
|
56
|
+
* expression), so the validator and coverage both pass theirs as they are. */
|
|
57
|
+
interface RunTreeNode {
|
|
58
|
+
id: string;
|
|
59
|
+
type: "group" | "snippet";
|
|
60
|
+
condition?: unknown;
|
|
61
|
+
selector?: Selector;
|
|
62
|
+
prompt?: unknown;
|
|
63
|
+
children?: readonly RunTreeNode[];
|
|
64
|
+
jump?: {
|
|
65
|
+
to: string;
|
|
66
|
+
mode?: "jump" | "call";
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A node that never runs, and why. Only the outermost such node is listed: everything inside it never
|
|
70
|
+
* runs either. */
|
|
71
|
+
type NeverRuns =
|
|
72
|
+
/** After an unconditioned snippet that jumps, in the same run. `jumper` is that snippet (inside a plain
|
|
73
|
+
* run group before this node, when that is where the jump is); `to` is where it jumps (`END` included). */
|
|
74
|
+
{
|
|
75
|
+
id: string;
|
|
76
|
+
type: "group" | "snippet";
|
|
77
|
+
reason: "after-jump";
|
|
78
|
+
jumper: string;
|
|
79
|
+
to: string;
|
|
80
|
+
}
|
|
81
|
+
/** In a `branch`, after a sibling with no condition, which is always picked first. */
|
|
82
|
+
| {
|
|
83
|
+
id: string;
|
|
84
|
+
type: "group" | "snippet";
|
|
85
|
+
reason: "branch-shadowed";
|
|
86
|
+
by: string;
|
|
87
|
+
};
|
|
88
|
+
/** The nodes in a block that can never run, in script order. `children` is the block's children (a run).
|
|
89
|
+
* Malformed shapes (a missing children array, a node that is not one) are skipped, never thrown on. */
|
|
90
|
+
declare function neverRunNodes(children: unknown): NeverRuns[];
|
|
91
|
+
|
|
53
92
|
interface SceneScaffold {
|
|
54
93
|
/** The scene's name: the card's title. */
|
|
55
94
|
name: string;
|
|
@@ -98,4 +137,4 @@ declare function planProject(opts: {
|
|
|
98
137
|
/** Plan a stub scene for a card. Pure: nothing is read or written. */
|
|
99
138
|
declare function planScene(target: ScenePlanTarget, scaffold: SceneScaffold): ScenePlan;
|
|
100
139
|
|
|
101
|
-
export { type ProjectInput, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, type ValidationIssue, canonicalStringify, hash32, hash4, humanizeNodeRefs, planProject, planScene, slug, validateProject };
|
|
140
|
+
export { type NeverRuns, type ProjectInput, type RunTreeNode, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, type ValidationIssue, canonicalStringify, hash32, hash4, humanizeNodeRefs, neverRunNodes, planProject, planScene, slug, validateProject };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { newId, parseSource } from '@wildwinter/toolkit';
|
|
2
|
-
import { ProjectFile, Scene, AuthoringFile } from '@patterkit/model';
|
|
2
|
+
import { ProjectFile, Scene, AuthoringFile, Selector } from '@patterkit/model';
|
|
3
3
|
export { effectiveGameId, gameIdify, isValidGameId } from '@patterkit/model';
|
|
4
4
|
|
|
5
5
|
/** Slugify an author name into a filename- / address-safe segment. */
|
|
@@ -26,7 +26,7 @@ interface StringifyOptions {
|
|
|
26
26
|
declare function canonicalStringify(value: unknown, opts?: StringifyOptions): string;
|
|
27
27
|
|
|
28
28
|
interface ValidationIssue {
|
|
29
|
-
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-speaker-label" | "unused-speaker-label" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "invalid-gamedata-value" | "jump-cycle";
|
|
29
|
+
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-speaker-label" | "unused-speaker-label" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "invalid-gamedata-value" | "jump-cycle" | "unreachable-after-jump" | "no-start" | "dangling-start";
|
|
30
30
|
message: string;
|
|
31
31
|
/** Id of the offending node/beat, where applicable. */
|
|
32
32
|
id?: string;
|
|
@@ -40,6 +40,8 @@ interface ValidationIssue {
|
|
|
40
40
|
bad: string;
|
|
41
41
|
options: string[];
|
|
42
42
|
};
|
|
43
|
+
/** `unreachable-after-jump`: the snippet whose jump means this node never runs (`id` is the node). */
|
|
44
|
+
jumper?: string;
|
|
43
45
|
}
|
|
44
46
|
interface ProjectInput {
|
|
45
47
|
project: ProjectFile;
|
|
@@ -50,6 +52,43 @@ interface ProjectInput {
|
|
|
50
52
|
/** Validate a project's structural invariants. Returns an empty array when valid. */
|
|
51
53
|
declare function validateProject(input: ProjectInput): ValidationIssue[];
|
|
52
54
|
|
|
55
|
+
/** The shape the walk reads: an authored node (condition as source text) or a compiled one (an
|
|
56
|
+
* expression), so the validator and coverage both pass theirs as they are. */
|
|
57
|
+
interface RunTreeNode {
|
|
58
|
+
id: string;
|
|
59
|
+
type: "group" | "snippet";
|
|
60
|
+
condition?: unknown;
|
|
61
|
+
selector?: Selector;
|
|
62
|
+
prompt?: unknown;
|
|
63
|
+
children?: readonly RunTreeNode[];
|
|
64
|
+
jump?: {
|
|
65
|
+
to: string;
|
|
66
|
+
mode?: "jump" | "call";
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A node that never runs, and why. Only the outermost such node is listed: everything inside it never
|
|
70
|
+
* runs either. */
|
|
71
|
+
type NeverRuns =
|
|
72
|
+
/** After an unconditioned snippet that jumps, in the same run. `jumper` is that snippet (inside a plain
|
|
73
|
+
* run group before this node, when that is where the jump is); `to` is where it jumps (`END` included). */
|
|
74
|
+
{
|
|
75
|
+
id: string;
|
|
76
|
+
type: "group" | "snippet";
|
|
77
|
+
reason: "after-jump";
|
|
78
|
+
jumper: string;
|
|
79
|
+
to: string;
|
|
80
|
+
}
|
|
81
|
+
/** In a `branch`, after a sibling with no condition, which is always picked first. */
|
|
82
|
+
| {
|
|
83
|
+
id: string;
|
|
84
|
+
type: "group" | "snippet";
|
|
85
|
+
reason: "branch-shadowed";
|
|
86
|
+
by: string;
|
|
87
|
+
};
|
|
88
|
+
/** The nodes in a block that can never run, in script order. `children` is the block's children (a run).
|
|
89
|
+
* Malformed shapes (a missing children array, a node that is not one) are skipped, never thrown on. */
|
|
90
|
+
declare function neverRunNodes(children: unknown): NeverRuns[];
|
|
91
|
+
|
|
53
92
|
interface SceneScaffold {
|
|
54
93
|
/** The scene's name: the card's title. */
|
|
55
94
|
name: string;
|
|
@@ -98,4 +137,4 @@ declare function planProject(opts: {
|
|
|
98
137
|
/** Plan a stub scene for a card. Pure: nothing is read or written. */
|
|
99
138
|
declare function planScene(target: ScenePlanTarget, scaffold: SceneScaffold): ScenePlan;
|
|
100
139
|
|
|
101
|
-
export { type ProjectInput, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, type ValidationIssue, canonicalStringify, hash32, hash4, humanizeNodeRefs, planProject, planScene, slug, validateProject };
|
|
140
|
+
export { type NeverRuns, type ProjectInput, type RunTreeNode, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, type ValidationIssue, canonicalStringify, hash32, hash4, humanizeNodeRefs, neverRunNodes, planProject, planScene, slug, validateProject };
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,49 @@ import { walkNodes, DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES, DEFAUL
|
|
|
59
59
|
import { isValidGameId, effectiveGameId } from "@patterkit/model";
|
|
60
60
|
import { isValidPropertyName, propertyNameify, RESERVED_PROPERTY_NAMES } from "@patterkit/model";
|
|
61
61
|
import { qualityDeclarationProblems, stagesEitherSide, valueFits } from "@wildwinter/expr";
|
|
62
|
+
|
|
63
|
+
// src/never-runs.ts
|
|
64
|
+
var isNode = (n) => !!n && typeof n === "object" && typeof n.id === "string";
|
|
65
|
+
function neverRunNodes(children) {
|
|
66
|
+
const out = [];
|
|
67
|
+
walkContainer(children, "run", out);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function walkContainer(children, selector, out) {
|
|
71
|
+
if (!Array.isArray(children)) return void 0;
|
|
72
|
+
if (selector === "run") {
|
|
73
|
+
let jumper;
|
|
74
|
+
for (const n of children) {
|
|
75
|
+
if (!isNode(n)) continue;
|
|
76
|
+
if (jumper !== void 0) {
|
|
77
|
+
out.push({ id: n.id, type: n.type, reason: "after-jump", ...jumper });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (n.type === "snippet") {
|
|
81
|
+
if (!n.condition && n.jump && typeof n.jump.to === "string" && n.jump.mode !== "call") jumper = { jumper: n.id, to: n.jump.to };
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const inner = walkContainer(n.children, n.selector ?? "run", out);
|
|
85
|
+
if (inner !== void 0 && !n.condition && !n.prompt && (n.selector ?? "run") === "run") jumper = inner;
|
|
86
|
+
}
|
|
87
|
+
return jumper;
|
|
88
|
+
}
|
|
89
|
+
let winner;
|
|
90
|
+
for (const n of children) {
|
|
91
|
+
if (!isNode(n)) continue;
|
|
92
|
+
if (selector === "branch") {
|
|
93
|
+
if (winner !== void 0) {
|
|
94
|
+
out.push({ id: n.id, type: n.type, reason: "branch-shadowed", by: winner });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!n.condition) winner = n.id;
|
|
98
|
+
}
|
|
99
|
+
if (n.type === "group") walkContainer(n.children, n.selector ?? "run", out);
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/validate.ts
|
|
62
105
|
function choiceLoopsBack(choice, ...targets) {
|
|
63
106
|
const back = new Set(targets.filter((s) => typeof s === "string" && s.length > 0));
|
|
64
107
|
const hits = (n) => n.type === "snippet" ? !!n.jump && back.has(n.jump.to) : (n.children ?? []).some(hits);
|
|
@@ -268,6 +311,17 @@ function validateProject(input) {
|
|
|
268
311
|
} else {
|
|
269
312
|
reportEmpty(block.children);
|
|
270
313
|
}
|
|
314
|
+
for (const n of neverRunNodes(block.children)) {
|
|
315
|
+
if (n.reason !== "after-jump") continue;
|
|
316
|
+
const where2 = n.to === "END" ? "to END" : `to '${n.to}'`;
|
|
317
|
+
issues.push({
|
|
318
|
+
code: "unreachable-after-jump",
|
|
319
|
+
severity: "warning",
|
|
320
|
+
id: n.id,
|
|
321
|
+
jumper: n.jumper,
|
|
322
|
+
message: `${n.type} '${n.id}' in block '${block.id}' never runs: snippet '${n.jumper}' before it always jumps ${where2}`
|
|
323
|
+
});
|
|
324
|
+
}
|
|
271
325
|
walkNodes(block.children, (node) => {
|
|
272
326
|
const inBlock = `block '${block.id}'`;
|
|
273
327
|
checkTags(node.tags, `${node.type} '${node.id}'`, node.id);
|
|
@@ -366,6 +420,32 @@ function validateProject(input) {
|
|
|
366
420
|
});
|
|
367
421
|
}
|
|
368
422
|
}
|
|
423
|
+
const wellFormed = scenes.filter((sc) => typeof sc?.id === "string" && Array.isArray(sc.blocks));
|
|
424
|
+
if (wellFormed.length > 0) {
|
|
425
|
+
const start = project.start;
|
|
426
|
+
const scene = start ? wellFormed.find((sc) => sc.id === start.scene) : void 0;
|
|
427
|
+
if (!start) {
|
|
428
|
+
issues.push({
|
|
429
|
+
code: "no-start",
|
|
430
|
+
severity: "warning",
|
|
431
|
+
message: "no start scene is set, so a game that doesn't name a scene starts at the first one"
|
|
432
|
+
});
|
|
433
|
+
} else if (!scene) {
|
|
434
|
+
issues.push({
|
|
435
|
+
code: "dangling-start",
|
|
436
|
+
severity: "warning",
|
|
437
|
+
id: start.scene,
|
|
438
|
+
message: `the start scene '${start.scene}' isn't in the project, so a game that doesn't name a scene starts at the first one`
|
|
439
|
+
});
|
|
440
|
+
} else if (start.block !== void 0 && !scene.blocks.some((b) => b?.id === start.block)) {
|
|
441
|
+
issues.push({
|
|
442
|
+
code: "dangling-start",
|
|
443
|
+
severity: "warning",
|
|
444
|
+
id: start.block,
|
|
445
|
+
message: `the start block '${start.block}' isn't in scene '${scene.id}', so the game starts at that scene's first block`
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
369
449
|
return issues;
|
|
370
450
|
}
|
|
371
451
|
function isQualifierEntry(q) {
|
|
@@ -737,6 +817,7 @@ export {
|
|
|
737
817
|
hash4,
|
|
738
818
|
humanizeNodeRefs,
|
|
739
819
|
isValidGameId2 as isValidGameId,
|
|
820
|
+
neverRunNodes,
|
|
740
821
|
newId,
|
|
741
822
|
parseSource,
|
|
742
823
|
planProject,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Patter core: stable id generation, readable-handle computation, canonical (de)serialisation, and the structural validator.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@patterkit/model": "0.
|
|
37
|
+
"@patterkit/model": "0.22.0",
|
|
38
38
|
"@wildwinter/expr": "^0.6.0",
|
|
39
|
-
"@wildwinter/toolkit": "^0.
|
|
39
|
+
"@wildwinter/toolkit": "^0.4.0",
|
|
40
40
|
"json5": "^2.2.3"
|
|
41
41
|
}
|
|
42
42
|
}
|