@nilvn/core 0.16.3 → 0.17.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/chunk-build.d.ts +10 -0
- package/dist/chunk-build.js +7 -5
- package/dist/commands.js +92 -1
- package/dist/ir.d.ts +71 -5
- package/dist/ir.js +26 -2
- package/dist/package.d.ts +40 -2
- package/dist/package.js +52 -5
- package/dist/schema.d.ts +4 -1
- package/dist/serialize.js +44 -12
- package/dist/versions.d.ts +1 -1
- package/package.json +1 -1
package/dist/chunk-build.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ export interface BuildChunkedOptions {
|
|
|
28
28
|
/** Command schema registry for serialization (`commandRegistry(manifests)`);
|
|
29
29
|
* built-ins only when omitted — see SerializeOptions.commands. */
|
|
30
30
|
commands?: Record<string, CommandSchema>;
|
|
31
|
+
/** A scoped build (the studio's preview of one scene): only these scene ids, in
|
|
32
|
+
* project order, go into the package — as ONE chunk, which is also the entry —
|
|
33
|
+
* and a jump or choice that leaves the scope routes to the unset landing
|
|
34
|
+
* (serialize's non-`crossChunk` mode) instead of naming a label the package
|
|
35
|
+
* does not carry. `groups` is ignored when set. Omitted = the whole project. */
|
|
36
|
+
scenes?: string[];
|
|
37
|
+
/** Emit `[label anchorLabel]` right before this node (play-from-here). */
|
|
38
|
+
anchorNodeId?: string;
|
|
39
|
+
/** The label to inject at `anchorNodeId`; defaults to `__nilvn_here__`. */
|
|
40
|
+
anchorLabel?: string;
|
|
31
41
|
}
|
|
32
42
|
/** Catalog keys a scene's runtime body references (say/narrate text + choice
|
|
33
43
|
* option labels) — i.e. the text that belongs in this scene's locale slice.
|
package/dist/chunk-build.js
CHANGED
|
@@ -57,10 +57,12 @@ export function sceneTextKeys(nodes, into) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
export function buildChunkedExport(project, opts) {
|
|
60
|
-
const
|
|
60
|
+
const scoped = opts.scenes ? project.scenes.filter((s) => opts.scenes.includes(s.id)) : project.scenes;
|
|
61
|
+
const sceneOrder = scoped.map((s) => s.id);
|
|
61
62
|
const sceneIds = new Set(sceneOrder);
|
|
62
|
-
const byId = new Map(
|
|
63
|
-
const groups = opts.groups ?? sceneOrder.map((id) => [id]);
|
|
63
|
+
const byId = new Map(scoped.map((s) => [s.id, s]));
|
|
64
|
+
const groups = opts.scenes ? [sceneOrder] : (opts.groups ?? sceneOrder.map((id) => [id]));
|
|
65
|
+
const anchor = opts.anchorNodeId ? { anchorNodeId: opts.anchorNodeId, ...(opts.anchorLabel ? { anchorLabel: opts.anchorLabel } : {}) } : {};
|
|
64
66
|
// Each group → one chunk. Chunk id = its first scene id (stable, unique).
|
|
65
67
|
const chunks = [];
|
|
66
68
|
const files = [];
|
|
@@ -76,7 +78,7 @@ export function buildChunkedExport(project, opts) {
|
|
|
76
78
|
if (!ids.length)
|
|
77
79
|
continue;
|
|
78
80
|
const chunkId = ids[0];
|
|
79
|
-
const { body, labels, assetRefs } = serializeChunk(project, { scenes: ids, keepKeys: true, crossChunk:
|
|
81
|
+
const { body, labels, assetRefs } = serializeChunk(project, { scenes: ids, keepKeys: true, crossChunk: !opts.scenes, ...anchor, ...(opts.commands ? { commands: opts.commands } : {}) });
|
|
80
82
|
const scriptChunk = { id: chunkId, body, labels };
|
|
81
83
|
const text = JSON.stringify(scriptChunk);
|
|
82
84
|
const bytes = utf8Len(text);
|
|
@@ -111,7 +113,7 @@ export function buildChunkedExport(project, opts) {
|
|
|
111
113
|
for (const sceneId of chunk.scenes) {
|
|
112
114
|
const scene = byId.get(sceneId);
|
|
113
115
|
for (const n of scene.nodes) {
|
|
114
|
-
if (n.kind === 'jump')
|
|
116
|
+
if (n.kind === 'jump' || n.kind === 'call')
|
|
115
117
|
addEdge(targetSceneId(n.target, sceneIds));
|
|
116
118
|
else if (n.kind === 'choice')
|
|
117
119
|
for (const o of n.options)
|
package/dist/commands.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Excluded on purpose:
|
|
5
5
|
// - use / alias / actor -> project setup (Project.plugins / alias / actors), not nodes
|
|
6
|
-
// - jump / if / set
|
|
6
|
+
// - jump / if / set / call / return -> first-class node kinds (JumpNode / JumpNode+condition / SetNode / CallNode / ReturnNode)
|
|
7
|
+
// - choices / persist -> fields of ChoiceNode / VariableDef (serialize.ts emits them)
|
|
8
|
+
/** The scene transitions `[trans]` and `[bg trans=]` know (the engine's TransitionKind). */
|
|
9
|
+
const TRANSITION_OPTIONS = ['fade', 'crossfade', 'wipe', 'slide', 'circle', 'blinds', 'rule'].map((value) => ({ value, label: `cmd.trans.kind.${value}` }));
|
|
10
|
+
const DIR_OPTIONS = ['left', 'right', 'up', 'down'].map((value) => ({ value, label: `cmd.trans.dir.${value}` }));
|
|
7
11
|
export const BUILTIN_COMMANDS = [
|
|
8
12
|
{
|
|
9
13
|
name: 'bg',
|
|
@@ -15,6 +19,93 @@ export const BUILTIN_COMMANDS = [
|
|
|
15
19
|
{ key: 'bg', label: 'cmd.bg.bg', type: 'asset:bg', positional: 0 },
|
|
16
20
|
{ key: 'color', label: 'cmd.bg.color', type: 'color' },
|
|
17
21
|
{ key: 'fade', label: 'cmd.bg.fade', type: 'number', default: 0 },
|
|
22
|
+
// `trans=` swaps through a scene transition instead of a cross-fade.
|
|
23
|
+
{ key: 'trans', label: 'cmd.bg.trans', type: 'enum', options: TRANSITION_OPTIONS, advanced: true },
|
|
24
|
+
{ key: 'dir', label: 'cmd.trans.dir', type: 'enum', options: DIR_OPTIONS, advanced: true },
|
|
25
|
+
{ key: 'duration', label: 'cmd.trans.duration', type: 'number', advanced: true },
|
|
26
|
+
{ key: 'mask', label: 'cmd.trans.mask', type: 'asset:bg', advanced: true },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'trans',
|
|
31
|
+
label: 'cmd.trans.label',
|
|
32
|
+
category: 'fx',
|
|
33
|
+
icon: '🎞',
|
|
34
|
+
hint: 'cmd.trans.hint',
|
|
35
|
+
params: [
|
|
36
|
+
{ key: 'kind', label: 'cmd.trans.kind', type: 'enum', required: true, positional: 0, default: 'fade', options: [...TRANSITION_OPTIONS, { value: 'end', label: 'cmd.trans.kind.end' }] },
|
|
37
|
+
{ key: 'duration', label: 'cmd.trans.duration', type: 'number', default: 0.6 },
|
|
38
|
+
{ key: 'dir', label: 'cmd.trans.dir', type: 'enum', options: DIR_OPTIONS },
|
|
39
|
+
{ key: 'color', label: 'cmd.trans.color', type: 'color', advanced: true },
|
|
40
|
+
{ key: 'mask', label: 'cmd.trans.mask', type: 'asset:bg', advanced: true },
|
|
41
|
+
{ key: 'softness', label: 'cmd.trans.softness', type: 'number', default: 0.1, advanced: true },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'preload',
|
|
46
|
+
label: 'cmd.preload.label',
|
|
47
|
+
category: 'stage',
|
|
48
|
+
icon: '⏳',
|
|
49
|
+
hint: 'cmd.preload.hint',
|
|
50
|
+
params: [
|
|
51
|
+
{ key: 'assets', label: 'cmd.preload.assets', type: 'string', required: true, positional: 0, list: true },
|
|
52
|
+
{ key: 'wait', label: 'cmd.preload.wait', type: 'boolean', default: false },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'ui',
|
|
57
|
+
label: 'cmd.ui.label',
|
|
58
|
+
category: 'stage',
|
|
59
|
+
icon: '🪟',
|
|
60
|
+
hint: 'cmd.ui.hint',
|
|
61
|
+
params: [
|
|
62
|
+
{
|
|
63
|
+
key: 'op',
|
|
64
|
+
label: 'cmd.ui.op',
|
|
65
|
+
type: 'enum',
|
|
66
|
+
required: true,
|
|
67
|
+
positional: 0,
|
|
68
|
+
default: 'show',
|
|
69
|
+
options: [
|
|
70
|
+
{ value: 'show', label: 'cmd.ui.op.show' },
|
|
71
|
+
{ value: 'hide', label: 'cmd.ui.op.hide' },
|
|
72
|
+
{ value: 'toggle', label: 'cmd.ui.op.toggle' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{ key: 'id', label: 'cmd.ui.id', type: 'panel', required: true, positional: 1 },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'hotspot',
|
|
80
|
+
label: 'cmd.hotspot.label',
|
|
81
|
+
category: 'stage',
|
|
82
|
+
icon: '🎯',
|
|
83
|
+
hint: 'cmd.hotspot.hint',
|
|
84
|
+
params: [
|
|
85
|
+
// `[hotspot clear]` / `[hotspot remove <id>]` reuse the id slot with a keyword.
|
|
86
|
+
{ key: 'id', label: 'cmd.hotspot.id', type: 'string', required: true, positional: 0 },
|
|
87
|
+
{ key: 'target', label: 'cmd.hotspot.target', type: 'string', positional: 1, advanced: true },
|
|
88
|
+
{ key: 'x', label: 'cmd.hotspot.x', type: 'number', default: 0 },
|
|
89
|
+
{ key: 'y', label: 'cmd.hotspot.y', type: 'number', default: 0 },
|
|
90
|
+
{ key: 'w', label: 'cmd.hotspot.w', type: 'number', default: 10 },
|
|
91
|
+
{ key: 'h', label: 'cmd.hotspot.h', type: 'number', default: 10 },
|
|
92
|
+
{ key: 'onclick', label: 'cmd.hotspot.onclick', type: 'script' },
|
|
93
|
+
{ key: 'if', label: 'cmd.hotspot.if', type: 'expr' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'input',
|
|
98
|
+
label: 'cmd.input.label',
|
|
99
|
+
category: 'flow',
|
|
100
|
+
icon: '⌨️',
|
|
101
|
+
hint: 'cmd.input.hint',
|
|
102
|
+
params: [
|
|
103
|
+
{ key: 'var', label: 'cmd.input.var', type: 'variable', required: true, positional: 0 },
|
|
104
|
+
{ key: 'prompt', label: 'cmd.input.prompt', type: 'key' },
|
|
105
|
+
{ key: 'default', label: 'cmd.input.default', type: 'string' },
|
|
106
|
+
{ key: 'persist', label: 'cmd.input.persist', type: 'boolean', default: false },
|
|
107
|
+
{ key: 'maxlength', label: 'cmd.input.maxlength', type: 'number', advanced: true },
|
|
108
|
+
{ key: 'pattern', label: 'cmd.input.pattern', type: 'string', advanced: true },
|
|
18
109
|
],
|
|
19
110
|
},
|
|
20
111
|
{
|
package/dist/ir.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
export type Lang = string;
|
|
2
2
|
/** textKey -> localized string. Values may carry inline markup ({wave:..}, {w:0.5}, {br}). */
|
|
3
3
|
export type TextCatalog = Record<string, string>;
|
|
4
|
+
/** The work's engine configuration — the JSON form of `nilvn.config.toml`,
|
|
5
|
+
* section by section (`title`, `ending`, `theme`, `window`, `menu`, `settings`,
|
|
6
|
+
* `keys`, `saves`, `choices`, `input`, `preload`, `ui`, `persist`, `strings`,
|
|
7
|
+
* `plugins.<id>` …). Core keeps it opaque on purpose: the engine is the one
|
|
8
|
+
* source of its meaning (its `CONFIG_SCHEMA` / `checkConfig` validate it and the
|
|
9
|
+
* studio's settings forms are generated from that schema), so nothing here
|
|
10
|
+
* duplicates the engine's types. Sections the package model carries elsewhere
|
|
11
|
+
* (`game.entry` / `game.scripts` / `path` / `actors` / `plugins.use`) do not
|
|
12
|
+
* belong in it — the engine drops them with a diagnostic. */
|
|
13
|
+
export interface WorkConfig {
|
|
14
|
+
[section: string]: unknown;
|
|
15
|
+
}
|
|
4
16
|
export interface Project {
|
|
5
17
|
meta: ProjectMeta;
|
|
6
18
|
actors: Record<string, Actor>;
|
|
@@ -18,6 +30,10 @@ export interface Project {
|
|
|
18
30
|
* the engine unlocks a segment when normal play passes its end. Optional: older
|
|
19
31
|
* projects simply have none. */
|
|
20
32
|
replays?: ReplaySegment[];
|
|
33
|
+
/** The work's engine configuration (see {@link WorkConfig}); travels into the
|
|
34
|
+
* script package as `nilvn.json` `config`. Optional: a project without one
|
|
35
|
+
* plays with the engine's defaults. Added at schema v12. */
|
|
36
|
+
config?: WorkConfig;
|
|
21
37
|
}
|
|
22
38
|
/** A node position id: which scene, which node — the timeline's stable locator
|
|
23
39
|
* (`NodeBase.id` is minted once and survives edits around it). */
|
|
@@ -73,8 +89,15 @@ export interface ReplaySegment {
|
|
|
73
89
|
* v11: plugin platform v2 — `PluginRef`
|
|
74
90
|
* is `{ id, version?, config? }` keyed by the plugin's reverse-DNS id; the
|
|
75
91
|
* migration maps bundled short names (`textfx` → `app.nilvn.textfx`) and keeps
|
|
76
|
-
* unknown names verbatim (the editor reports them, nothing is dropped).
|
|
77
|
-
|
|
92
|
+
* unknown names verbatim (the editor reports them, nothing is dropped).
|
|
93
|
+
* v12: `Project.config` — the work's engine configuration (the JSON form of
|
|
94
|
+
* nilvn.config.toml), exported as the package's `config`. Pure addition.
|
|
95
|
+
* v13: layered sprites and the actor's plugin fields — `Actor.textColor` /
|
|
96
|
+
* `canvas` / `layers` / `ext` (pure additions) and `Actor.voice` moved under
|
|
97
|
+
* `ext['app.nilvn.voicefx']` (migrated); `ChoiceNode.timer` / `timerDefault`,
|
|
98
|
+
* `ChoiceOption.disabled`, `VariableDef.persist`, `CallNode` / `ReturnNode`
|
|
99
|
+
* (pure additions). */
|
|
100
|
+
export declare const CURRENT_SCHEMA_VERSION = 13;
|
|
78
101
|
/** Bring a loaded project up to CURRENT_SCHEMA_VERSION in place (then return it).
|
|
79
102
|
* Call once on load, after reading from disk. Runs every table step the project
|
|
80
103
|
* predates, in order, then stamps the current version. */
|
|
@@ -111,18 +134,41 @@ export interface SceneMapChapter {
|
|
|
111
134
|
sceneIds: string[];
|
|
112
135
|
collapsed?: boolean;
|
|
113
136
|
}
|
|
137
|
+
/** One layer of a layered sprite. `src` is a path template with `{<layer>}`
|
|
138
|
+
* for the value (`char/yuki/body-{body}.webp`); `values` is the editor's
|
|
139
|
+
* picker list — the `face` layer's values are the actor's `faces`. */
|
|
140
|
+
export interface ActorLayer {
|
|
141
|
+
src: string;
|
|
142
|
+
/** The value shown until a command sets one. */
|
|
143
|
+
default?: string;
|
|
144
|
+
/** Where a cropped layer image sits on the canvas, in canvas pixels. */
|
|
145
|
+
offset?: [number, number];
|
|
146
|
+
/** May be unset (`none` clears it). */
|
|
147
|
+
optional?: boolean;
|
|
148
|
+
values?: string[];
|
|
149
|
+
}
|
|
114
150
|
export interface Actor {
|
|
115
151
|
id: string;
|
|
116
152
|
/** Display name is localizable, hence a catalog key. */
|
|
117
153
|
nameKey: string;
|
|
154
|
+
/** Name-tag BACKGROUND colour. */
|
|
118
155
|
color?: string;
|
|
156
|
+
/** Name-tag TEXT colour (the theme's `name-color` when absent). */
|
|
157
|
+
textColor?: string;
|
|
119
158
|
/** Sprite URL template; `{face}` is replaced by the current face. */
|
|
120
159
|
sprites: string;
|
|
121
160
|
/** Available faces, for the editor's expression picker. */
|
|
122
161
|
faces: string[];
|
|
123
162
|
defaultFace?: string;
|
|
124
|
-
/**
|
|
125
|
-
|
|
163
|
+
/** Layered sprite: the shared canvas in image pixels the layers align on. */
|
|
164
|
+
canvas?: [number, number];
|
|
165
|
+
/** Layered sprite: named layers composed bottom to top in declaration order.
|
|
166
|
+
* `face` is the layer `say.face` / `[char id face]` drive; the others change
|
|
167
|
+
* through `[char id body=casual]`. Wins over `sprites` when present. */
|
|
168
|
+
layers?: Record<string, ActorLayer>;
|
|
169
|
+
/** Plugin-declared fields by plugin id (`contributes.actorFields`) — the
|
|
170
|
+
* voicefx pitch lives at `ext['app.nilvn.voicefx'].voice` (v13; was `voice`). */
|
|
171
|
+
ext?: Record<string, Record<string, unknown>>;
|
|
126
172
|
/** The character's reproducible recipe (provenance):
|
|
127
173
|
* typically `source:'face-creator'`, carrying the whole def so any pose can be
|
|
128
174
|
* re-rendered at conversion time (the expression-set "registration template" a
|
|
@@ -136,6 +182,10 @@ export interface VariableDef {
|
|
|
136
182
|
default: number | boolean | string;
|
|
137
183
|
/** Display name in the editor. */
|
|
138
184
|
label?: string;
|
|
185
|
+
/** Kept across saves, restarts and runs (the engine's `[persist]` table: the
|
|
186
|
+
* stored value wins, `default` seeds the first run). Exported into the
|
|
187
|
+
* package config's `persist` section, never as a script command. */
|
|
188
|
+
persist?: boolean;
|
|
139
189
|
}
|
|
140
190
|
export interface ResourceRegistry {
|
|
141
191
|
backgrounds: AssetRef[];
|
|
@@ -216,7 +266,7 @@ export interface Scene {
|
|
|
216
266
|
titleKey: string;
|
|
217
267
|
nodes: SceneNode[];
|
|
218
268
|
}
|
|
219
|
-
export type SceneNode = SayNode | NarrateNode | CommandNode | ChoiceNode | SetNode | JumpNode | LabelNode | RecallNode | AnimNode | EventFrameNode | LoopStartNode | LoopStopNode;
|
|
269
|
+
export type SceneNode = SayNode | NarrateNode | CommandNode | ChoiceNode | SetNode | JumpNode | CallNode | ReturnNode | LabelNode | RecallNode | AnimNode | EventFrameNode | LoopStartNode | LoopStopNode;
|
|
220
270
|
export type NodeKind = SceneNode['kind'];
|
|
221
271
|
interface NodeBase {
|
|
222
272
|
/** Stable id generated by the editor; jumps/references point at it. */
|
|
@@ -250,12 +300,19 @@ export interface CommandNode extends NodeBase {
|
|
|
250
300
|
export interface ChoiceNode extends NodeBase {
|
|
251
301
|
kind: 'choice';
|
|
252
302
|
options: ChoiceOption[];
|
|
303
|
+
/** Seconds the prompt waits before picking `timerDefault` by itself (the
|
|
304
|
+
* engine's `[choices timer=]`); absent = what the `[choices]` config says. */
|
|
305
|
+
timer?: number;
|
|
306
|
+
/** The option a timeout picks, counted from 1 among the shown options. */
|
|
307
|
+
timerDefault?: number;
|
|
253
308
|
}
|
|
254
309
|
export interface ChoiceOption {
|
|
255
310
|
labelKey: string;
|
|
256
311
|
target: JumpTarget;
|
|
257
312
|
/** Shown only when truthy. */
|
|
258
313
|
condition?: string;
|
|
314
|
+
/** Shown greyed and unpickable when truthy (`disabled=`). */
|
|
315
|
+
disabled?: string;
|
|
259
316
|
}
|
|
260
317
|
export interface SetNode extends NodeBase {
|
|
261
318
|
kind: 'set';
|
|
@@ -266,6 +323,15 @@ export interface JumpNode extends NodeBase {
|
|
|
266
323
|
kind: 'jump';
|
|
267
324
|
target: JumpTarget;
|
|
268
325
|
}
|
|
326
|
+
/** `[call label]`: jump there and come back at the next `[return]` (calls nest). */
|
|
327
|
+
export interface CallNode extends NodeBase {
|
|
328
|
+
kind: 'call';
|
|
329
|
+
target: JumpTarget;
|
|
330
|
+
}
|
|
331
|
+
/** `[return]`: back to the line after the last `[call]`. */
|
|
332
|
+
export interface ReturnNode extends NodeBase {
|
|
333
|
+
kind: 'return';
|
|
334
|
+
}
|
|
269
335
|
export interface LabelNode extends NodeBase {
|
|
270
336
|
kind: 'label';
|
|
271
337
|
name: string;
|
package/dist/ir.js
CHANGED
|
@@ -40,8 +40,15 @@ import { resolvePluginId } from './plugins.js';
|
|
|
40
40
|
* v11: plugin platform v2 — `PluginRef`
|
|
41
41
|
* is `{ id, version?, config? }` keyed by the plugin's reverse-DNS id; the
|
|
42
42
|
* migration maps bundled short names (`textfx` → `app.nilvn.textfx`) and keeps
|
|
43
|
-
* unknown names verbatim (the editor reports them, nothing is dropped).
|
|
44
|
-
|
|
43
|
+
* unknown names verbatim (the editor reports them, nothing is dropped).
|
|
44
|
+
* v12: `Project.config` — the work's engine configuration (the JSON form of
|
|
45
|
+
* nilvn.config.toml), exported as the package's `config`. Pure addition.
|
|
46
|
+
* v13: layered sprites and the actor's plugin fields — `Actor.textColor` /
|
|
47
|
+
* `canvas` / `layers` / `ext` (pure additions) and `Actor.voice` moved under
|
|
48
|
+
* `ext['app.nilvn.voicefx']` (migrated); `ChoiceNode.timer` / `timerDefault`,
|
|
49
|
+
* `ChoiceOption.disabled`, `VariableDef.persist`, `CallNode` / `ReturnNode`
|
|
50
|
+
* (pure additions). */
|
|
51
|
+
export const CURRENT_SCHEMA_VERSION = 13;
|
|
45
52
|
/** The stable id a ref (of either shape) points at. */
|
|
46
53
|
function refId(r) {
|
|
47
54
|
const raw = r.id ?? r.name;
|
|
@@ -152,6 +159,23 @@ const MIGRATIONS = [
|
|
|
152
159
|
p.plugins = next;
|
|
153
160
|
},
|
|
154
161
|
},
|
|
162
|
+
// v11 -> v12: `Project.config` (the work's engine configuration). Pure addition
|
|
163
|
+
// — a project without one plays with the engine's defaults.
|
|
164
|
+
// v12 -> v13: the voicefx pitch (`Actor.voice`) moves under `ext['app.nilvn.voicefx']`
|
|
165
|
+
// like every plugin actor field; the layered-sprite fields are pure additions.
|
|
166
|
+
{
|
|
167
|
+
to: 13,
|
|
168
|
+
run: (p) => {
|
|
169
|
+
for (const a of Object.values(p.actors)) {
|
|
170
|
+
const legacy = a;
|
|
171
|
+
if (legacy.voice === undefined)
|
|
172
|
+
continue;
|
|
173
|
+
const ext = (a.ext ??= {});
|
|
174
|
+
ext['app.nilvn.voicefx'] = { ...ext['app.nilvn.voicefx'], voice: legacy.voice };
|
|
175
|
+
delete legacy.voice;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
},
|
|
155
179
|
];
|
|
156
180
|
/** Bring a loaded project up to CURRENT_SCHEMA_VERSION in place (then return it).
|
|
157
181
|
* Call once on load, after reading from disk. Runs every table step the project
|
package/dist/package.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Project } from './ir.js';
|
|
1
|
+
import type { Actor, Project, WorkConfig } from './ir.js';
|
|
2
2
|
import { type ChunkManifest, type ManifestAsset } from './chunk.js';
|
|
3
3
|
import { type BuildChunkedOptions, type ChunkFile } from './chunk-build.js';
|
|
4
4
|
/** Script-package format version. Bumped only when nilvn.json's shape changes
|
|
@@ -12,13 +12,32 @@ export declare const PACKAGE_MANIFEST_FILE = "nilvn.json";
|
|
|
12
12
|
export interface PackageActor {
|
|
13
13
|
name?: string;
|
|
14
14
|
nameKey?: string;
|
|
15
|
+
/** Name-tag background colour. */
|
|
15
16
|
color?: string;
|
|
17
|
+
/** Name-tag text colour. */
|
|
18
|
+
textColor?: string;
|
|
16
19
|
/** Sprite URL template; `{face}` is replaced by the current face. */
|
|
17
20
|
sprites?: string;
|
|
18
21
|
defaultFace?: string;
|
|
19
|
-
/**
|
|
22
|
+
/** Layered sprite: the shared canvas in image pixels (`[600, 1100]`). */
|
|
23
|
+
canvas?: [number, number];
|
|
24
|
+
/** Layered sprite: named layers composed bottom to top (`face` follows the
|
|
25
|
+
* face; the others change through `[char id body=casual]`). Wins over `sprites`. */
|
|
26
|
+
layers?: Record<string, PackageActorLayer>;
|
|
27
|
+
/** Plugin actor fields by plugin id (`contributes.actorFields`), e.g.
|
|
28
|
+
* `ext['app.nilvn.voicefx'].voice`. */
|
|
29
|
+
ext?: Record<string, Record<string, unknown>>;
|
|
30
|
+
/** @deprecated — the voicefx pitch; the engine moves it under `ext`. */
|
|
20
31
|
voice?: number;
|
|
21
32
|
}
|
|
33
|
+
/** One layer of a layered sprite in the package (the engine's `ActorLayerDef`). */
|
|
34
|
+
export interface PackageActorLayer {
|
|
35
|
+
/** Path template; `{<layer>}` is replaced by the layer's value. */
|
|
36
|
+
src: string;
|
|
37
|
+
default?: string;
|
|
38
|
+
offset?: [number, number];
|
|
39
|
+
optional?: boolean;
|
|
40
|
+
}
|
|
22
41
|
/** One enabled plugin, by its reverse-DNS id (`app.nilvn.textfx`; the engine
|
|
23
42
|
* also accepts the short names older packages carry).
|
|
24
43
|
* `entry` = a plugin carried inside the package (its `plugin.json`, relative to
|
|
@@ -49,6 +68,14 @@ export interface PackageManifest {
|
|
|
49
68
|
textSpeed: number;
|
|
50
69
|
/** Per-work id the runtime namespaces saves / settings by. */
|
|
51
70
|
saveKey: string;
|
|
71
|
+
/** The work's engine configuration — the JSON form of `nilvn.config.toml`
|
|
72
|
+
* (`Project.config`). The engine applies it once the package is open, after
|
|
73
|
+
* the asset table is filled, so a skin, logo or background in it resolves by
|
|
74
|
+
* ref like any other asset. Optional (a package without one plays with the
|
|
75
|
+
* engine's defaults); the sections `nilvn.json` carries itself (`game.entry`
|
|
76
|
+
* / `game.scripts` / `path` / `actors` / `plugins.use`) are dropped with a
|
|
77
|
+
* diagnostic. Engines before 0.17 ignore the field. */
|
|
78
|
+
config?: WorkConfig;
|
|
52
79
|
/** The chunk manifest (chunk.ts), embedded as-is: chunk / locale-slice / asset
|
|
53
80
|
* index + entry label. Wire files (`chunks/**`, `assets/**`) are unchanged. */
|
|
54
81
|
chunks: ChunkManifest;
|
|
@@ -74,6 +101,8 @@ export interface BuildPackageOptions extends BuildChunkedOptions {
|
|
|
74
101
|
textSpeed?: number;
|
|
75
102
|
/** Defaults to the project id, else the title. */
|
|
76
103
|
saveKey?: string;
|
|
104
|
+
/** Defaults to `project.config`; left out of the manifest when empty. */
|
|
105
|
+
config?: WorkConfig;
|
|
77
106
|
}
|
|
78
107
|
export interface ScriptPackagePlan {
|
|
79
108
|
/** nilvn.json, with `chunks.assets` still EMPTY — the producer resolves the
|
|
@@ -88,6 +117,8 @@ export interface ScriptPackagePlan {
|
|
|
88
117
|
/** Languages the finished work can switch between: the default plus every
|
|
89
118
|
* declared language that ships a non-empty catalog. The default comes first. */
|
|
90
119
|
export declare function packageLanguages(project: Project): string[];
|
|
120
|
+
/** The layer table minus the editor's picker lists. */
|
|
121
|
+
export declare function packageLayers(layers: NonNullable<Actor['layers']>): Record<string, PackageActorLayer>;
|
|
91
122
|
/** The runtime actor table straight from the IR (`name` = the default-language
|
|
92
123
|
* display name, falling back to the id). */
|
|
93
124
|
export declare function packageActors(project: Project): Record<string, PackageActor>;
|
|
@@ -96,5 +127,12 @@ export declare function packageActors(project: Project): Record<string, PackageA
|
|
|
96
127
|
* single-file / asset-ZIP shape) plus the manifest fields the shells used to bake
|
|
97
128
|
* into their bootstraps. Pure, zero-I/O. */
|
|
98
129
|
export declare function buildScriptPackage(project: Project, opts: BuildPackageOptions): ScriptPackagePlan;
|
|
130
|
+
/** Asset refs a work configuration names — a skin, logo, background, music,
|
|
131
|
+
* HUD icon or preload entry — found by walking every string in it with the
|
|
132
|
+
* same predicate the scene walk uses (`isAssetRef`), so a producer resolves
|
|
133
|
+
* them into the package's by-ref table alongside the scenes' assets. Strings
|
|
134
|
+
* holding a `{placeholder}` (a layered actor's `src` template) are templates,
|
|
135
|
+
* not refs, and are left to the actor table. */
|
|
136
|
+
export declare function configAssetRefs(config: WorkConfig | undefined): string[];
|
|
99
137
|
/** Fill the by-ref asset table (`chunks.assets`). Returns a new manifest. */
|
|
100
138
|
export declare function fillPackageAssets(manifest: PackageManifest, assets: Record<string, ManifestAsset>): PackageManifest;
|
package/dist/package.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Pure types + a builder over buildChunkedExport, zero-dep. The editor's four
|
|
9
9
|
// exports (single-file HTML / asset ZIP / chunked ZIP / .nvpk) are this package
|
|
10
10
|
// plus a shell; the engine's `load()` is its one consumer.
|
|
11
|
+
import { isAssetRef } from './serialize.js';
|
|
11
12
|
import { isChunkManifest } from './chunk.js';
|
|
12
13
|
import { buildChunkedExport } from './chunk-build.js';
|
|
13
14
|
/** Script-package format version. Bumped only when nilvn.json's shape changes
|
|
@@ -42,13 +43,31 @@ export function packageLanguages(project) {
|
|
|
42
43
|
const extra = declared.filter((l) => l !== def && Object.keys(project.catalogs[l] ?? {}).length > 0);
|
|
43
44
|
return [def, ...extra];
|
|
44
45
|
}
|
|
46
|
+
/** The layer table minus the editor's picker lists. */
|
|
47
|
+
export function packageLayers(layers) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [name, l] of Object.entries(layers)) {
|
|
50
|
+
out[name] = { src: l.src, ...(l.default ? { default: l.default } : {}), ...(l.offset ? { offset: l.offset } : {}), ...(l.optional ? { optional: true } : {}) };
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
45
54
|
/** The runtime actor table straight from the IR (`name` = the default-language
|
|
46
55
|
* display name, falling back to the id). */
|
|
47
56
|
export function packageActors(project) {
|
|
48
57
|
const cat = project.catalogs[project.meta.defaultLang] ?? {};
|
|
49
58
|
const out = {};
|
|
50
59
|
for (const [id, a] of Object.entries(project.actors)) {
|
|
51
|
-
out[id] = {
|
|
60
|
+
out[id] = {
|
|
61
|
+
name: cat[a.nameKey] ?? id,
|
|
62
|
+
nameKey: a.nameKey,
|
|
63
|
+
color: a.color,
|
|
64
|
+
sprites: a.sprites,
|
|
65
|
+
defaultFace: a.defaultFace,
|
|
66
|
+
...(a.textColor ? { textColor: a.textColor } : {}),
|
|
67
|
+
...(a.canvas ? { canvas: a.canvas } : {}),
|
|
68
|
+
...(a.layers ? { layers: packageLayers(a.layers) } : {}),
|
|
69
|
+
...(a.ext && Object.keys(a.ext).length ? { ext: a.ext } : {}),
|
|
70
|
+
};
|
|
52
71
|
}
|
|
53
72
|
return out;
|
|
54
73
|
}
|
|
@@ -57,12 +76,14 @@ export function packageActors(project) {
|
|
|
57
76
|
* single-file / asset-ZIP shape) plus the manifest fields the shells used to bake
|
|
58
77
|
* into their bootstraps. Pure, zero-I/O. */
|
|
59
78
|
export function buildScriptPackage(project, opts) {
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
79
|
+
// The chunk options ride through whole (registry, grouping, a preview's scene
|
|
80
|
+
// scope and anchor): without the command registry the chunks serialize plugin
|
|
81
|
+
// commands against the built-ins alone, and a positional argument
|
|
82
|
+
// (`[move yuki …]`) degrades to `id=yuki`, which the plugin never reads.
|
|
83
|
+
const plan = buildChunkedExport(project, opts);
|
|
64
84
|
const title = opts.title ?? (project.meta.title || 'NilVN');
|
|
65
85
|
const lang = (opts.lang ?? project.meta.defaultLang);
|
|
86
|
+
const config = opts.config ?? project.config;
|
|
66
87
|
const manifest = {
|
|
67
88
|
format: PACKAGE_FORMAT,
|
|
68
89
|
title,
|
|
@@ -73,10 +94,36 @@ export function buildScriptPackage(project, opts) {
|
|
|
73
94
|
plugins: opts.plugins ?? project.plugins.map((p) => ({ id: p.id })),
|
|
74
95
|
textSpeed: opts.textSpeed ?? (Number(project.meta.textSpeed) || 40),
|
|
75
96
|
saveKey: opts.saveKey ?? (project.meta.id || project.meta.title || 'nilvn'),
|
|
97
|
+
...(config && Object.keys(config).length ? { config } : {}),
|
|
76
98
|
chunks: plan.manifest,
|
|
77
99
|
};
|
|
78
100
|
return { manifest, files: plan.files, assetRefs: plan.assetRefs };
|
|
79
101
|
}
|
|
102
|
+
/** Asset refs a work configuration names — a skin, logo, background, music,
|
|
103
|
+
* HUD icon or preload entry — found by walking every string in it with the
|
|
104
|
+
* same predicate the scene walk uses (`isAssetRef`), so a producer resolves
|
|
105
|
+
* them into the package's by-ref table alongside the scenes' assets. Strings
|
|
106
|
+
* holding a `{placeholder}` (a layered actor's `src` template) are templates,
|
|
107
|
+
* not refs, and are left to the actor table. */
|
|
108
|
+
export function configAssetRefs(config) {
|
|
109
|
+
const out = new Set();
|
|
110
|
+
const walk = (v) => {
|
|
111
|
+
if (typeof v === 'string') {
|
|
112
|
+
if (isAssetRef(v) && !v.includes('{'))
|
|
113
|
+
out.add(v);
|
|
114
|
+
}
|
|
115
|
+
else if (Array.isArray(v)) {
|
|
116
|
+
for (const x of v)
|
|
117
|
+
walk(x);
|
|
118
|
+
}
|
|
119
|
+
else if (v && typeof v === 'object') {
|
|
120
|
+
for (const x of Object.values(v))
|
|
121
|
+
walk(x);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
walk(config);
|
|
125
|
+
return [...out];
|
|
126
|
+
}
|
|
80
127
|
/** Fill the by-ref asset table (`chunks.assets`). Returns a new manifest. */
|
|
81
128
|
export function fillPackageAssets(manifest, assets) {
|
|
82
129
|
return { ...manifest, chunks: { ...manifest.chunks, assets } };
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ParamType = 'string' | 'number' | 'boolean' | 'enum' | 'asset:bg' | 'asset:bgm' | 'asset:se' | 'asset:sprite' | 'actor' | 'face' | 'color' | 'expr';
|
|
1
|
+
export type ParamType = 'string' | 'number' | 'boolean' | 'enum' | 'asset:bg' | 'asset:bgm' | 'asset:se' | 'asset:sprite' | 'actor' | 'face' | 'color' | 'expr' | 'variable' | 'panel' | 'key' | 'script';
|
|
2
2
|
export interface ParamOption {
|
|
3
3
|
value: string;
|
|
4
4
|
label: string;
|
|
@@ -18,6 +18,9 @@ export interface ParamSchema {
|
|
|
18
18
|
positional?: number;
|
|
19
19
|
/** Tuck behind an "advanced" disclosure in the editor. */
|
|
20
20
|
advanced?: boolean;
|
|
21
|
+
/** A positional that holds several values: the value is whitespace-separated
|
|
22
|
+
* and serializes as that many positional tokens (`[preload a.png b.wav]`). */
|
|
23
|
+
list?: boolean;
|
|
21
24
|
}
|
|
22
25
|
export type CommandCategory = 'stage' | 'audio' | 'fx' | 'flow' | 'text';
|
|
23
26
|
export interface CommandSchema {
|
package/dist/serialize.js
CHANGED
|
@@ -169,13 +169,28 @@ function serializeNode(node, text, commands, scope = null) {
|
|
|
169
169
|
}
|
|
170
170
|
case 'narrate':
|
|
171
171
|
return `|${text(node.textKey)}`;
|
|
172
|
-
case 'choice':
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.join('
|
|
172
|
+
case 'choice': {
|
|
173
|
+
// `if=` / `disabled=` are read to the tag's end and unquoted by the engine
|
|
174
|
+
// (they carry spaces), so they go last and raw. A timer rides a `[choices]`
|
|
175
|
+
// line right before the prompt.
|
|
176
|
+
const lines = node.options.map((o) => {
|
|
177
|
+
const guards = [o.condition ? `if=${o.condition}` : '', o.disabled ? `disabled=${o.disabled}` : ''].filter(Boolean);
|
|
178
|
+
return `[choice ${token(text(o.labelKey))} -> ${dest(o.target, scope)}${guards.length ? ' ' + guards.join(' ') : ''}]`;
|
|
179
|
+
});
|
|
180
|
+
if (node.timer !== undefined || node.timerDefault !== undefined) {
|
|
181
|
+
const parts = ['choices'];
|
|
182
|
+
if (node.timer !== undefined)
|
|
183
|
+
parts.push(`timer=${node.timer}`);
|
|
184
|
+
if (node.timerDefault !== undefined)
|
|
185
|
+
parts.push(`default=${node.timerDefault}`);
|
|
186
|
+
lines.unshift(`[${parts.join(' ')}]`);
|
|
187
|
+
}
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
case 'call':
|
|
191
|
+
return `[call ${dest(node.target, scope)}]`;
|
|
192
|
+
case 'return':
|
|
193
|
+
return '[return]';
|
|
179
194
|
case 'jump':
|
|
180
195
|
// v1 DSL expresses a conditional jump as [if cond -> label].
|
|
181
196
|
return node.condition
|
|
@@ -188,7 +203,7 @@ function serializeNode(node, text, commands, scope = null) {
|
|
|
188
203
|
// gallery is built from the IR directly, not from the runtime script.
|
|
189
204
|
return `; recall ${node.recallId}`;
|
|
190
205
|
case 'command':
|
|
191
|
-
return serializeCommand(node, commands[node.cmd]);
|
|
206
|
+
return serializeCommand(node, commands[node.cmd], text);
|
|
192
207
|
case 'anim':
|
|
193
208
|
return serializeAnim(node);
|
|
194
209
|
case 'eventframe':
|
|
@@ -203,10 +218,18 @@ function serializeNode(node, text, commands, scope = null) {
|
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
}
|
|
206
|
-
function serializeCommand(node, schema) {
|
|
221
|
+
function serializeCommand(node, schema, text) {
|
|
207
222
|
const parts = [node.cmd];
|
|
208
223
|
const params = node.params;
|
|
209
224
|
const used = new Set();
|
|
225
|
+
// A `key` param holds a catalog `@key`; under keepKeys the engine resolves it,
|
|
226
|
+
// otherwise the literal text goes out like a dialogue line's.
|
|
227
|
+
const value = (p, v) => {
|
|
228
|
+
if (p.type === 'key' && text && typeof v === 'string' && v.startsWith('@'))
|
|
229
|
+
return text(v.slice(1));
|
|
230
|
+
return v;
|
|
231
|
+
};
|
|
232
|
+
const raw = [];
|
|
210
233
|
if (schema) {
|
|
211
234
|
const positionals = schema.params
|
|
212
235
|
.filter((p) => p.positional !== undefined)
|
|
@@ -221,7 +244,12 @@ function serializeCommand(node, schema) {
|
|
|
221
244
|
for (let i = 0; i <= last; i++) {
|
|
222
245
|
const p = positionals[i];
|
|
223
246
|
used.add(p.key);
|
|
224
|
-
|
|
247
|
+
const v = value(p, params[p.key] ?? p.default ?? '');
|
|
248
|
+
if (p.list)
|
|
249
|
+
for (const item of String(v).split(/\s+/).filter(Boolean))
|
|
250
|
+
parts.push(token(item));
|
|
251
|
+
else
|
|
252
|
+
parts.push(token(v));
|
|
225
253
|
}
|
|
226
254
|
for (const p of schema.params) {
|
|
227
255
|
if (p.positional !== undefined)
|
|
@@ -232,7 +260,11 @@ function serializeCommand(node, schema) {
|
|
|
232
260
|
continue;
|
|
233
261
|
if (p.default !== undefined && val === p.default)
|
|
234
262
|
continue;
|
|
235
|
-
|
|
263
|
+
// A condition is read to the tag's end unquoted: it goes last, raw.
|
|
264
|
+
if (p.type === 'expr')
|
|
265
|
+
raw.push(`${p.key}=${String(val)}`);
|
|
266
|
+
else
|
|
267
|
+
parts.push(`${p.key}=${token(value(p, val))}`);
|
|
236
268
|
}
|
|
237
269
|
}
|
|
238
270
|
// Params not described by the schema (e.g. unknown plugin command) -> key=value.
|
|
@@ -241,7 +273,7 @@ function serializeCommand(node, schema) {
|
|
|
241
273
|
continue;
|
|
242
274
|
parts.push(`${k}=${token(v)}`);
|
|
243
275
|
}
|
|
244
|
-
return `[${parts.join(' ')}]`;
|
|
276
|
+
return `[${[...parts, ...raw].join(' ')}]`;
|
|
245
277
|
}
|
|
246
278
|
// A recorded keyframe animation (AnimNode) serializes to an `[anim …]` command the
|
|
247
279
|
// engine's built-in `anim` handler plays. The whole track rides one compact `kf`
|
package/dist/versions.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const FORMAT_VERSIONS: Readonly<{
|
|
2
2
|
/** Project / IR document (`ProjectMeta.schemaVersion`) — `migrateProject` brings
|
|
3
3
|
* older projects forward, one table step per bump. */
|
|
4
|
-
irSchema:
|
|
4
|
+
irSchema: 13;
|
|
5
5
|
/** Chunked-streaming manifest (`ChunkManifest.format`) — the loaders gate on it. */
|
|
6
6
|
chunkManifest: 1;
|
|
7
7
|
/** Script package (`nilvn.json` `format`) — `checkPackageManifest` gates on it. */
|
package/package.json
CHANGED