@nilvn/core 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/catalog.d.ts +54 -0
- package/dist/catalog.js +137 -0
- package/dist/chunk-build.d.ts +37 -0
- package/dist/chunk-build.js +187 -0
- package/dist/chunk.d.ts +106 -0
- package/dist/chunk.js +35 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +181 -0
- package/dist/i18n.d.ts +50 -0
- package/dist/i18n.js +77 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/ir.d.ts +407 -0
- package/dist/ir.js +167 -0
- package/dist/package.d.ts +100 -0
- package/dist/package.js +80 -0
- package/dist/plugin-manifest.d.ts +146 -0
- package/dist/plugin-manifest.js +165 -0
- package/dist/plugins.d.ts +26 -0
- package/dist/plugins.js +42 -0
- package/dist/schema.d.ts +73 -0
- package/dist/schema.js +15 -0
- package/dist/screenplay-format.d.ts +8 -0
- package/dist/screenplay-format.js +96 -0
- package/dist/screenplay.d.ts +99 -0
- package/dist/screenplay.js +161 -0
- package/dist/semver.d.ts +15 -0
- package/dist/semver.js +129 -0
- package/dist/serialize.d.ts +63 -0
- package/dist/serialize.js +365 -0
- package/dist/versions.d.ts +17 -0
- package/dist/versions.js +25 -0
- package/package.json +42 -0
package/dist/ir.d.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
export type Lang = string;
|
|
2
|
+
/** textKey -> localized string. Values may carry inline markup ({wave:..}, {w:0.5}, {br}). */
|
|
3
|
+
export type TextCatalog = Record<string, string>;
|
|
4
|
+
export interface Project {
|
|
5
|
+
meta: ProjectMeta;
|
|
6
|
+
actors: Record<string, Actor>;
|
|
7
|
+
variables: VariableDef[];
|
|
8
|
+
resources: ResourceRegistry;
|
|
9
|
+
plugins: PluginRef[];
|
|
10
|
+
scenes: Scene[];
|
|
11
|
+
catalogs: Record<Lang, TextCatalog>;
|
|
12
|
+
/** Project-level reusable loop cycles (the loop library). Authoring metadata only — these never serialize into the playable
|
|
13
|
+
* script (a `LoopStartNode` inlines its own body). Optional: older projects and
|
|
14
|
+
* freshly-created ones simply have none. */
|
|
15
|
+
loopClips?: LoopClip[];
|
|
16
|
+
/** Authored A–B replay segments (the replay gallery, schema v7). Each records
|
|
17
|
+
* two node position ids; the serializer emits a start label + an end marker, and
|
|
18
|
+
* the engine unlocks a segment when normal play passes its end. Optional: older
|
|
19
|
+
* projects simply have none. */
|
|
20
|
+
replays?: ReplaySegment[];
|
|
21
|
+
}
|
|
22
|
+
/** A node position id: which scene, which node — the timeline's stable locator
|
|
23
|
+
* (`NodeBase.id` is minted once and survives edits around it). */
|
|
24
|
+
export interface NodeAnchor {
|
|
25
|
+
sceneId: string;
|
|
26
|
+
nodeId: string;
|
|
27
|
+
}
|
|
28
|
+
/** An authored A–B replay segment: plays from the `start` node through the `end`
|
|
29
|
+
* node (inclusive). Unlocked for the player's replay gallery once normal play
|
|
30
|
+
* passes the end marker. The end anchor should be a node normal flow actually
|
|
31
|
+
* passes THROUGH (a dialogue / command) — a choice or jump node leaves before
|
|
32
|
+
* the marker after it would run. */
|
|
33
|
+
export interface ReplaySegment {
|
|
34
|
+
id: string;
|
|
35
|
+
/** Localized gallery title (catalog key, like scene titles). */
|
|
36
|
+
titleKey: string;
|
|
37
|
+
start: NodeAnchor;
|
|
38
|
+
end: NodeAnchor;
|
|
39
|
+
}
|
|
40
|
+
/** Current IR / project format version. Bump when the on-disk IR shape changes
|
|
41
|
+
* in a way that needs a migration. Independent of the studio app version and of
|
|
42
|
+
* `ProjectMeta.version` (the author's own content version).
|
|
43
|
+
*
|
|
44
|
+
* v2: `plugins` became the authoritative enabled-set (empty = all disabled).
|
|
45
|
+
* Pre-v2 projects always ran every bundled plugin, so the migration fills
|
|
46
|
+
* an empty `plugins` with all bundled plugins enabled. See migrateProject.
|
|
47
|
+
* v3: `resources.spritesheets` added (sprite-frame animation).
|
|
48
|
+
* v4: `AnimNode` (recorded keyframe animations) joined the SceneNode union — a
|
|
49
|
+
* pure addition (older projects simply have none), so no data migration; the
|
|
50
|
+
* bump only marks the format so an older reader knows it's newer.
|
|
51
|
+
* v5: the recording animation redesign joined the union:
|
|
52
|
+
* `EventFrameNode` (scene-level event-frame) and `LoopStartNode` /
|
|
53
|
+
* `LoopStopNode` (single-element async loops), plus the project-level
|
|
54
|
+
* `loopClips` loop library. All pure additions (older projects have none), so no
|
|
55
|
+
* data migration; the bump only stamps the format. The whole redesign ships as
|
|
56
|
+
* one unreleased batch, hence one schema version covering every new node kind
|
|
57
|
+
* (and the loop library) rather than a bump per increment.
|
|
58
|
+
* v6: asset provenance metadata — optional `provenance` /
|
|
59
|
+
* `box` / `canonicalName` on `AssetRef`, and optional `origin` on `Actor`. The
|
|
60
|
+
* groundwork for the Web→Pro conversion (where a vector SVG placeholder is the
|
|
61
|
+
* registration template a raster replacement aligns onto). All pure additions
|
|
62
|
+
* (older projects / assets simply have none = unknown source), so no data
|
|
63
|
+
* migration; the bump only stamps the format. Authoring-only metadata — it
|
|
64
|
+
* never serializes into the playable script (the DSL carries only the resolved
|
|
65
|
+
* asset path; see serialize.ts).
|
|
66
|
+
* v7: `Project.replays` — authored A–B replay segments (two node
|
|
67
|
+
* position ids each). A pure optional addition (older projects have none), so
|
|
68
|
+
* no data migration; the bump only stamps the format. Unlike most authoring
|
|
69
|
+
* metadata these DO reach the runtime script: serialize.ts emits a
|
|
70
|
+
* `[replaydef]` preamble, a start label and an end marker per segment.
|
|
71
|
+
* v8–v10: the pluginization backfills (abreplay / animstudio / voicerecord became
|
|
72
|
+
* plugins) — see the migration table.
|
|
73
|
+
* v11: plugin platform v2 — `PluginRef`
|
|
74
|
+
* is `{ id, version?, config? }` keyed by the plugin's reverse-DNS id; the
|
|
75
|
+
* migration maps bundled short names (`textfx` → `app.nilvn.textfx`) and keeps
|
|
76
|
+
* unknown names verbatim (the editor reports them, nothing is dropped). */
|
|
77
|
+
export declare const CURRENT_SCHEMA_VERSION = 11;
|
|
78
|
+
/** Bring a loaded project up to CURRENT_SCHEMA_VERSION in place (then return it).
|
|
79
|
+
* Call once on load, after reading from disk. Runs every table step the project
|
|
80
|
+
* predates, in order, then stamps the current version. */
|
|
81
|
+
export declare function migrateProject(project: Project): Project;
|
|
82
|
+
export interface ProjectMeta {
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
/** The author's own content version of their work (not the format/app version). */
|
|
86
|
+
version: string;
|
|
87
|
+
defaultLang: Lang;
|
|
88
|
+
languages: Lang[];
|
|
89
|
+
/** Default typewriter speed in characters per second. */
|
|
90
|
+
textSpeed?: number;
|
|
91
|
+
/** Release resolution imported images are compressed to (default 1440p). */
|
|
92
|
+
shipRes?: '1080p' | '1440p' | '2160p';
|
|
93
|
+
/** Editor-only UI state. It does not affect playback semantics. */
|
|
94
|
+
editor?: EditorProjectMeta;
|
|
95
|
+
/** IR / project format version, for gating future format migrations.
|
|
96
|
+
* Backfilled to CURRENT_SCHEMA_VERSION on load when absent. */
|
|
97
|
+
schemaVersion?: number;
|
|
98
|
+
}
|
|
99
|
+
export interface EditorProjectMeta {
|
|
100
|
+
sceneMap?: {
|
|
101
|
+
positions?: Record<string, {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
}>;
|
|
105
|
+
chapters?: SceneMapChapter[];
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export interface SceneMapChapter {
|
|
109
|
+
id: string;
|
|
110
|
+
title: string;
|
|
111
|
+
sceneIds: string[];
|
|
112
|
+
collapsed?: boolean;
|
|
113
|
+
}
|
|
114
|
+
export interface Actor {
|
|
115
|
+
id: string;
|
|
116
|
+
/** Display name is localizable, hence a catalog key. */
|
|
117
|
+
nameKey: string;
|
|
118
|
+
color?: string;
|
|
119
|
+
/** Sprite URL template; `{face}` is replaced by the current face. */
|
|
120
|
+
sprites: string;
|
|
121
|
+
/** Available faces, for the editor's expression picker. */
|
|
122
|
+
faces: string[];
|
|
123
|
+
defaultFace?: string;
|
|
124
|
+
/** Base pitch (Hz) for the voicefx plugin. */
|
|
125
|
+
voice?: number;
|
|
126
|
+
/** The character's reproducible recipe (provenance):
|
|
127
|
+
* typically `source:'face-creator'`, carrying the whole def so any pose can be
|
|
128
|
+
* re-rendered at conversion time (the expression-set "registration template" a
|
|
129
|
+
* raster sprite later aligns onto). Authoring-only and optional; absent = unknown
|
|
130
|
+
* origin. Never reaches the engine wire format. */
|
|
131
|
+
origin?: AssetProvenance;
|
|
132
|
+
}
|
|
133
|
+
export interface VariableDef {
|
|
134
|
+
name: string;
|
|
135
|
+
type: 'number' | 'boolean' | 'string';
|
|
136
|
+
default: number | boolean | string;
|
|
137
|
+
/** Display name in the editor. */
|
|
138
|
+
label?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface ResourceRegistry {
|
|
141
|
+
backgrounds: AssetRef[];
|
|
142
|
+
audio: AudioRef[];
|
|
143
|
+
/** Sprite-frame animation sheets (single-row strips), imported losslessly.
|
|
144
|
+
* Added at schema v3; older projects backfill an empty list on migrate. */
|
|
145
|
+
spritesheets: AssetRef[];
|
|
146
|
+
}
|
|
147
|
+
/** A face-creator character recipe — the `buildCharacterDef` product
|
|
148
|
+
* (`format:'nilvn-face-character'` + base selection/colors + included poses). Core
|
|
149
|
+
* treats it as an OPAQUE payload: it only carries the recipe so the editor /
|
|
150
|
+
* face-creator can re-render any pose later (`applyPose` derives all 8 expressions),
|
|
151
|
+
* and never interprets its internals. Kept structural (not imported from the
|
|
152
|
+
* face-creator package) so @nilvn/core stays zero-dependency and decoupled — the
|
|
153
|
+
* face-creator is a separate sibling whose consumption (vendor / publish / symlink)
|
|
154
|
+
* is still undecided (STRATEGY §8). */
|
|
155
|
+
export interface FaceRecipe {
|
|
156
|
+
format: 'nilvn-face-character';
|
|
157
|
+
[k: string]: unknown;
|
|
158
|
+
}
|
|
159
|
+
/** Where an asset's bytes came from. A discriminated union so the Web→Pro converter
|
|
160
|
+
* can offer the right replacement lane per source (re-render from a recipe, re-run /
|
|
161
|
+
* condition an AI prompt, swap a library id, or accept a manual raster drop). */
|
|
162
|
+
export type AssetProvenance = {
|
|
163
|
+
source: 'face-creator';
|
|
164
|
+
def: FaceRecipe;
|
|
165
|
+
} | {
|
|
166
|
+
source: 'ai-svg';
|
|
167
|
+
prompt: string;
|
|
168
|
+
model?: string;
|
|
169
|
+
} | {
|
|
170
|
+
source: 'builtin';
|
|
171
|
+
libId: string;
|
|
172
|
+
} | {
|
|
173
|
+
source: 'imported';
|
|
174
|
+
};
|
|
175
|
+
/** An asset's intrinsic render box — its native canvas plus an optional anchor — so a
|
|
176
|
+
* replacement can be scaled and positioned to land exactly where the SVG placeholder
|
|
177
|
+
* sat (alignment is where asset replacement most easily breaks; STRATEGY §6). */
|
|
178
|
+
export interface RenderBox {
|
|
179
|
+
/** Intrinsic canvas size (a character = the face-creator 600×1100). */
|
|
180
|
+
w: number;
|
|
181
|
+
h: number;
|
|
182
|
+
/** Anchor in canvas coords (a character's feet / center; from face-creator ANCHOR). */
|
|
183
|
+
anchor?: {
|
|
184
|
+
x: number;
|
|
185
|
+
y: number;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export interface AssetRef {
|
|
189
|
+
id: string;
|
|
190
|
+
path: string;
|
|
191
|
+
thumb?: string;
|
|
192
|
+
/** Where this asset came from (provenance,). Optional and
|
|
193
|
+
* authoring-only; absent = unknown source. */
|
|
194
|
+
provenance?: AssetProvenance;
|
|
195
|
+
/** Intrinsic render box, for aligning a replacement onto this asset. */
|
|
196
|
+
box?: RenderBox;
|
|
197
|
+
/** Canonical filename (e.g. `yuki__happy.png`) so a commission pack's files
|
|
198
|
+
* auto-match back to their slots with no manual mapping (STRATEGY §6). */
|
|
199
|
+
canonicalName?: string;
|
|
200
|
+
}
|
|
201
|
+
export interface AudioRef extends AssetRef {
|
|
202
|
+
kind: 'bgm' | 'se';
|
|
203
|
+
}
|
|
204
|
+
/** One enabled plugin (schema v11, plugin platform v2). `id` is the manifest's
|
|
205
|
+
* reverse-DNS id (`app.nilvn.textfx`); the hosts resolve bundled ids from their
|
|
206
|
+
* registries and third-party ones from the project's plugin packages. */
|
|
207
|
+
export interface PluginRef {
|
|
208
|
+
id: string;
|
|
209
|
+
/** The version the project was authored against (informational; the host
|
|
210
|
+
* loads whatever version it has and reports a mismatch). */
|
|
211
|
+
version?: string;
|
|
212
|
+
config?: Record<string, unknown>;
|
|
213
|
+
}
|
|
214
|
+
export interface Scene {
|
|
215
|
+
id: string;
|
|
216
|
+
titleKey: string;
|
|
217
|
+
nodes: SceneNode[];
|
|
218
|
+
}
|
|
219
|
+
export type SceneNode = SayNode | NarrateNode | CommandNode | ChoiceNode | SetNode | JumpNode | LabelNode | RecallNode | AnimNode | EventFrameNode | LoopStartNode | LoopStopNode;
|
|
220
|
+
export type NodeKind = SceneNode['kind'];
|
|
221
|
+
interface NodeBase {
|
|
222
|
+
/** Stable id generated by the editor; jumps/references point at it. */
|
|
223
|
+
id: string;
|
|
224
|
+
/** Optional guard expression over project variables; node runs only when truthy. */
|
|
225
|
+
condition?: string;
|
|
226
|
+
}
|
|
227
|
+
export interface SayNode extends NodeBase {
|
|
228
|
+
kind: 'say';
|
|
229
|
+
actor: string;
|
|
230
|
+
face?: string;
|
|
231
|
+
textKey: string;
|
|
232
|
+
/** Optional per-line voice clip (an AssetPath). Plays as the line appears;
|
|
233
|
+
* serialized to a `[voice <path> offset=<sec>]` just before the dialogue, so
|
|
234
|
+
* the engine plays it alongside the line and mutes the synth typing blip. */
|
|
235
|
+
voice?: string;
|
|
236
|
+
/** Start-time correction (seconds) for the voice clip: playback seeks here to
|
|
237
|
+
* skip leading silence. Defaults to 0. */
|
|
238
|
+
voiceOffset?: number;
|
|
239
|
+
}
|
|
240
|
+
export interface NarrateNode extends NodeBase {
|
|
241
|
+
kind: 'narrate';
|
|
242
|
+
textKey: string;
|
|
243
|
+
}
|
|
244
|
+
export interface CommandNode extends NodeBase {
|
|
245
|
+
kind: 'command';
|
|
246
|
+
cmd: string;
|
|
247
|
+
/** Named values; described and validated by the command schema (see schema.ts). */
|
|
248
|
+
params: Record<string, string | number | boolean>;
|
|
249
|
+
}
|
|
250
|
+
export interface ChoiceNode extends NodeBase {
|
|
251
|
+
kind: 'choice';
|
|
252
|
+
options: ChoiceOption[];
|
|
253
|
+
}
|
|
254
|
+
export interface ChoiceOption {
|
|
255
|
+
labelKey: string;
|
|
256
|
+
target: JumpTarget;
|
|
257
|
+
/** Shown only when truthy. */
|
|
258
|
+
condition?: string;
|
|
259
|
+
}
|
|
260
|
+
export interface SetNode extends NodeBase {
|
|
261
|
+
kind: 'set';
|
|
262
|
+
var: string;
|
|
263
|
+
expr: string;
|
|
264
|
+
}
|
|
265
|
+
export interface JumpNode extends NodeBase {
|
|
266
|
+
kind: 'jump';
|
|
267
|
+
target: JumpTarget;
|
|
268
|
+
}
|
|
269
|
+
export interface LabelNode extends NodeBase {
|
|
270
|
+
kind: 'label';
|
|
271
|
+
name: string;
|
|
272
|
+
}
|
|
273
|
+
/** Recollection / CG-gallery marker; the editor builds the replay list from these. */
|
|
274
|
+
export interface RecallNode extends NodeBase {
|
|
275
|
+
kind: 'recall';
|
|
276
|
+
recallId: string;
|
|
277
|
+
titleKey: string;
|
|
278
|
+
thumb?: string;
|
|
279
|
+
}
|
|
280
|
+
/** One keyframe of an AnimNode. `t` is seconds from the clip start. The transform
|
|
281
|
+
* channels mirror the engine's fixed schema: `x` / `y` are pixel translate offsets
|
|
282
|
+
* from the object's birth position, `scale` / `rotation` / `opacity` are absolute.
|
|
283
|
+
* Every keyframe of a clip carries the same channel set (the recorded tracks), so
|
|
284
|
+
* playback never carries a value forward. `ease` is a short easing code for the
|
|
285
|
+
* segment INTO the next keyframe (expanded by the engine; see builtins `[anim]`). */
|
|
286
|
+
export interface AnimKeyframe {
|
|
287
|
+
t: number;
|
|
288
|
+
x?: number;
|
|
289
|
+
y?: number;
|
|
290
|
+
scale?: number;
|
|
291
|
+
rotation?: number;
|
|
292
|
+
opacity?: number;
|
|
293
|
+
ease?: string;
|
|
294
|
+
}
|
|
295
|
+
/** A recorded keyframe animation played on one stage object. A first-class node with its own editor (not a CommandNode);
|
|
296
|
+
* serializes to an `[anim …]` command for the engine. `hold` persists the end pose
|
|
297
|
+
* into the object's resting transform; absent = transient, snaps back. */
|
|
298
|
+
export interface AnimNode extends NodeBase {
|
|
299
|
+
kind: 'anim';
|
|
300
|
+
/** Target stage object id: `character:<id>` / `sprite:<id>` / `camera`. */
|
|
301
|
+
target: string;
|
|
302
|
+
/** Total clip length in seconds; keyframe `t` values lie in [0, duration]. */
|
|
303
|
+
duration: number;
|
|
304
|
+
keyframes: AnimKeyframe[];
|
|
305
|
+
/** Persist the end pose. Absent / false = transient, snaps back. */
|
|
306
|
+
hold?: boolean;
|
|
307
|
+
}
|
|
308
|
+
/** A recordable channel's value. Continuous channels (x/y/scale/rotation/opacity)
|
|
309
|
+
* carry numbers and are interpolated; discrete channels (visible/face/layer/…)
|
|
310
|
+
* carry strings or booleans and snap at the keyframe time. Which channels a kind
|
|
311
|
+
* exposes is declared by the engine's `ObjectKind.recordable` ("dynamic
|
|
312
|
+
* extraction"), so new object types become keyframable with no IR change. */
|
|
313
|
+
export type ChannelValue = number | string | boolean;
|
|
314
|
+
/** One keyframe of an `ElementTrack`. `t` is seconds from the event-frame start.
|
|
315
|
+
* `ch` is SPARSE — only the channels changed at this keyframe (a missing channel
|
|
316
|
+
* carries the previous keyframe's value). Unlike `[anim]`'s decimation, identity values
|
|
317
|
+
* are kept, so re-editing knows which channels were recorded. `ease` is the
|
|
318
|
+
* easing code for the segment INTO the next keyframe of the SAME channel
|
|
319
|
+
* (continuous channels only; discrete channels snap regardless). */
|
|
320
|
+
export interface Keyframe {
|
|
321
|
+
t: number;
|
|
322
|
+
ch: Record<string, ChannelValue>;
|
|
323
|
+
ease?: string;
|
|
324
|
+
}
|
|
325
|
+
/** One stage object's keyframe track inside an `EventFrameNode`. */
|
|
326
|
+
export interface ElementTrack {
|
|
327
|
+
objId: string;
|
|
328
|
+
keys: Keyframe[];
|
|
329
|
+
}
|
|
330
|
+
/** A scene-level "recording event-frame": a one-shot, blocking, multi-element
|
|
331
|
+
* keyframe choreography over a real time-track. Each
|
|
332
|
+
* track's `keys[0]` is kf0 — the full state snapshot captured when recording
|
|
333
|
+
* began. A first-class node with its own editor (not a CommandNode); serializes
|
|
334
|
+
* to an `[eventframe …]` command the engine plays through a pure-JS rAF clock.
|
|
335
|
+
* The save only records the node position (the event-frame is atomic), so a
|
|
336
|
+
* load jumps to kf0 — mid-play progress is never persisted. */
|
|
337
|
+
export interface EventFrameNode extends NodeBase {
|
|
338
|
+
kind: 'eventframe';
|
|
339
|
+
/** Total clip length in seconds; keyframe `t` values lie in [0, duration]. */
|
|
340
|
+
duration: number;
|
|
341
|
+
tracks: ElementTrack[];
|
|
342
|
+
}
|
|
343
|
+
/** Begin a single-element loop: an async,
|
|
344
|
+
* non-blocking, repeating animation on one stage object that runs across later
|
|
345
|
+
* beats until a matching `LoopStopNode` (or a new loop on the same object, or a
|
|
346
|
+
* load / restart). Three parts, split so the cycle can be reused independently:
|
|
347
|
+
* `entry` (the anchor pose set when the loop begins) + `body` (the reusable cycle,
|
|
348
|
+
* played on repeat) + the `exit` carried by the stop node. Serializes to a
|
|
349
|
+
* `[loopstart …]` command. */
|
|
350
|
+
export interface LoopStartNode extends NodeBase {
|
|
351
|
+
kind: 'loopstart';
|
|
352
|
+
/** Target stage object id: `character:<id>` / `sprite:<id>` / `camera`. */
|
|
353
|
+
objId: string;
|
|
354
|
+
/** Cycle length in seconds; `body` keyframe `t` values lie in [0, duration].
|
|
355
|
+
* Continuous channels wrap seamlessly when `body[0]` equals `body[last]`. */
|
|
356
|
+
duration: number;
|
|
357
|
+
/** The loop's anchor pose — channel values the object is set to when the loop
|
|
358
|
+
* begins (typically equal to `body[0]`, plus any channels the body holds
|
|
359
|
+
* constant). Continuous values are numbers, discrete values strings/booleans. */
|
|
360
|
+
entry: Record<string, ChannelValue>;
|
|
361
|
+
/** The reusable cycle: sparse keyframes (same grammar as an `ElementTrack`),
|
|
362
|
+
* played on repeat. Kept structurally separate from `entry` so the same body can
|
|
363
|
+
* drive a project-level named-loop library in a later increment. */
|
|
364
|
+
body: Keyframe[];
|
|
365
|
+
/** Easing code for the bridge from the current pose into `entry` (reserved for
|
|
366
|
+
* the bridge-in tween; the runtime snaps to `entry` until that lands). */
|
|
367
|
+
intoEase?: string;
|
|
368
|
+
}
|
|
369
|
+
/** Stop the single-element loop running on `objId` and settle it. Serializes to a `[loopstop …]` command. */
|
|
370
|
+
export interface LoopStopNode extends NodeBase {
|
|
371
|
+
kind: 'loopstop';
|
|
372
|
+
/** Which running loop to stop (by target object id). */
|
|
373
|
+
objId: string;
|
|
374
|
+
/** The pose the object settles to when the loop stops; committed to the resting
|
|
375
|
+
* model so it persists. */
|
|
376
|
+
exit: Record<string, ChannelValue>;
|
|
377
|
+
/** Easing code for the bridge from the loop pose out to `exit` (reserved for the
|
|
378
|
+
* bridge-out tween; the runtime snaps to `exit` until that lands). */
|
|
379
|
+
outEase?: string;
|
|
380
|
+
}
|
|
381
|
+
/** A reusable recorded loop cycle (the project-level loop library). Authoring-only metadata — it is NOT part of the engine wire format:
|
|
382
|
+
* a `LoopStartNode` inlines its own absolute `body`, so a clip is just the source the
|
|
383
|
+
* editor materializes that body from. Persists with the project (so the author can
|
|
384
|
+
* pick it on later beats), but never serializes into the playable script.
|
|
385
|
+
*
|
|
386
|
+
* `body` keyframes hold values **relative to the recording's start pose** (deltas, with
|
|
387
|
+
* `body[0]` at delta 0). On insert the editor rebases them onto the target element's
|
|
388
|
+
* current pose — `entry[ch] + delta` — so one clip drives the same motion wherever (and
|
|
389
|
+
* on whichever recordable element) it is dropped (this is what "the current state is the first frame"
|
|
390
|
+
* means in practice). Only continuous channels are captured today (a drag samples
|
|
391
|
+
* x/y/scale/rotation/opacity); discrete loops are a later addition. */
|
|
392
|
+
export interface LoopClip {
|
|
393
|
+
id: string;
|
|
394
|
+
/** Author-facing label, shown in the insert picker (auto "Loop N", editable). */
|
|
395
|
+
name: string;
|
|
396
|
+
/** Cycle length in seconds; `body` keyframe `t` values lie in [0, duration]. */
|
|
397
|
+
duration: number;
|
|
398
|
+
/** Sparse delta keyframes (same grammar as an `ElementTrack`'s keys). */
|
|
399
|
+
body: Keyframe[];
|
|
400
|
+
}
|
|
401
|
+
export interface JumpTarget {
|
|
402
|
+
/** Omitted = current scene; set = cross-scene (cross-file) jump. */
|
|
403
|
+
scene?: string;
|
|
404
|
+
/** Target LabelNode.name. */
|
|
405
|
+
label: string;
|
|
406
|
+
}
|
|
407
|
+
export {};
|
package/dist/ir.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// NilVN editor document model (IR) — the single source of truth for the creator studio.
|
|
2
|
+
// Consumed by the editor, AI generation, the engine (via compile) and the bundler.
|
|
3
|
+
// Value import (not type-only): resolvePluginId is used by
|
|
4
|
+
// migrateProject below. plugins.ts imports only *types* from here, so there's no
|
|
5
|
+
// runtime import cycle.
|
|
6
|
+
import { resolvePluginId } from './plugins.js';
|
|
7
|
+
/** Current IR / project format version. Bump when the on-disk IR shape changes
|
|
8
|
+
* in a way that needs a migration. Independent of the studio app version and of
|
|
9
|
+
* `ProjectMeta.version` (the author's own content version).
|
|
10
|
+
*
|
|
11
|
+
* v2: `plugins` became the authoritative enabled-set (empty = all disabled).
|
|
12
|
+
* Pre-v2 projects always ran every bundled plugin, so the migration fills
|
|
13
|
+
* an empty `plugins` with all bundled plugins enabled. See migrateProject.
|
|
14
|
+
* v3: `resources.spritesheets` added (sprite-frame animation).
|
|
15
|
+
* v4: `AnimNode` (recorded keyframe animations) joined the SceneNode union — a
|
|
16
|
+
* pure addition (older projects simply have none), so no data migration; the
|
|
17
|
+
* bump only marks the format so an older reader knows it's newer.
|
|
18
|
+
* v5: the recording animation redesign joined the union:
|
|
19
|
+
* `EventFrameNode` (scene-level event-frame) and `LoopStartNode` /
|
|
20
|
+
* `LoopStopNode` (single-element async loops), plus the project-level
|
|
21
|
+
* `loopClips` loop library. All pure additions (older projects have none), so no
|
|
22
|
+
* data migration; the bump only stamps the format. The whole redesign ships as
|
|
23
|
+
* one unreleased batch, hence one schema version covering every new node kind
|
|
24
|
+
* (and the loop library) rather than a bump per increment.
|
|
25
|
+
* v6: asset provenance metadata — optional `provenance` /
|
|
26
|
+
* `box` / `canonicalName` on `AssetRef`, and optional `origin` on `Actor`. The
|
|
27
|
+
* groundwork for the Web→Pro conversion (where a vector SVG placeholder is the
|
|
28
|
+
* registration template a raster replacement aligns onto). All pure additions
|
|
29
|
+
* (older projects / assets simply have none = unknown source), so no data
|
|
30
|
+
* migration; the bump only stamps the format. Authoring-only metadata — it
|
|
31
|
+
* never serializes into the playable script (the DSL carries only the resolved
|
|
32
|
+
* asset path; see serialize.ts).
|
|
33
|
+
* v7: `Project.replays` — authored A–B replay segments (two node
|
|
34
|
+
* position ids each). A pure optional addition (older projects have none), so
|
|
35
|
+
* no data migration; the bump only stamps the format. Unlike most authoring
|
|
36
|
+
* metadata these DO reach the runtime script: serialize.ts emits a
|
|
37
|
+
* `[replaydef]` preamble, a start label and an end marker per segment.
|
|
38
|
+
* v8–v10: the pluginization backfills (abreplay / animstudio / voicerecord became
|
|
39
|
+
* plugins) — see the migration table.
|
|
40
|
+
* v11: plugin platform v2 — `PluginRef`
|
|
41
|
+
* is `{ id, version?, config? }` keyed by the plugin's reverse-DNS id; the
|
|
42
|
+
* migration maps bundled short names (`textfx` → `app.nilvn.textfx`) and keeps
|
|
43
|
+
* unknown names verbatim (the editor reports them, nothing is dropped). */
|
|
44
|
+
export const CURRENT_SCHEMA_VERSION = 11;
|
|
45
|
+
/** The stable id a ref (of either shape) points at. */
|
|
46
|
+
function refId(r) {
|
|
47
|
+
const raw = r.id ?? r.name;
|
|
48
|
+
return raw ? resolvePluginId(raw) : undefined;
|
|
49
|
+
}
|
|
50
|
+
function hasPlugin(p, id) {
|
|
51
|
+
return p.plugins.some((x) => refId(x) === id);
|
|
52
|
+
}
|
|
53
|
+
/** The migration table, oldest first. Each schema bump adds one entry here (or a
|
|
54
|
+
* comment when it is a pure addition). Table-driven so the
|
|
55
|
+
* steps read as data. */
|
|
56
|
+
/** The content plugins a pre-v2 project (empty enabled set = "everything on") is
|
|
57
|
+
* backfilled with. FROZEN history: this is what "all first-party content
|
|
58
|
+
* plugins" meant when the migration was written, by id — the live first-party
|
|
59
|
+
* inventory lives in @nilvn/plugins and must not steer a migration. */
|
|
60
|
+
const LEGACY_DEFAULT_PLUGINS = [
|
|
61
|
+
'app.nilvn.textfx',
|
|
62
|
+
'app.nilvn.charfx',
|
|
63
|
+
'app.nilvn.choicefx',
|
|
64
|
+
'app.nilvn.screenfx',
|
|
65
|
+
'app.nilvn.objectfx',
|
|
66
|
+
'app.nilvn.spriteanim',
|
|
67
|
+
'app.nilvn.voicefx',
|
|
68
|
+
'app.nilvn.voicerecord',
|
|
69
|
+
'app.nilvn.animstudio',
|
|
70
|
+
'app.nilvn.abreplay',
|
|
71
|
+
];
|
|
72
|
+
const MIGRATIONS = [
|
|
73
|
+
// v1 -> v2: `plugins` is now the enabled-set. Pre-v2 always ran every bundled
|
|
74
|
+
// plugin, so an empty list then meant "all on" — fill it to preserve behavior.
|
|
75
|
+
// (At v2+ an empty list legitimately means "all disabled" and is left alone.)
|
|
76
|
+
{ to: 2, run: (p) => { if (p.plugins.length === 0)
|
|
77
|
+
p.plugins = LEGACY_DEFAULT_PLUGINS.map((id) => ({ id })); } },
|
|
78
|
+
// v2 -> v3: `resources.spritesheets` is a new asset category (sprite-frame
|
|
79
|
+
// animation). Pre-v3 projects have no field — backfill an empty list so every
|
|
80
|
+
// reader can assume it exists.
|
|
81
|
+
{ to: 3, run: (p) => { p.resources.spritesheets ??= []; } },
|
|
82
|
+
// v3 -> v4: `AnimNode` joined the SceneNode union. Pure addition — nothing to backfill.
|
|
83
|
+
// v4 -> v5: the recording animation redesign joined the union (`EventFrameNode`,
|
|
84
|
+
// `LoopStartNode`, `LoopStopNode`). Pure additions.
|
|
85
|
+
// v5 -> v6: asset provenance metadata (provenance/box/canonicalName on AssetRef,
|
|
86
|
+
// origin on Actor). Optional pure additions — a missing field just means "unknown source".
|
|
87
|
+
// v6 -> v7: `Project.replays` (A–B replay segments). Optional pure addition.
|
|
88
|
+
// v7 -> v8: A–B replay became the `abreplay` plugin; before v8 it was
|
|
89
|
+
// always-on. Projects that USE it (authored `replays`) get it enabled
|
|
90
|
+
// unconditionally — their exported gallery/unlocks must keep working. Projects
|
|
91
|
+
// that don't get it only when their enabled set is non-empty (keeping the
|
|
92
|
+
// replay tab they were used to seeing); a deliberately emptied set stays
|
|
93
|
+
// authoritative all-off, as it has been since v2.
|
|
94
|
+
{
|
|
95
|
+
to: 8,
|
|
96
|
+
run: (p, ctx) => {
|
|
97
|
+
if ((p.replays?.length || ctx.hadPlugins) && !hasPlugin(p, 'app.nilvn.abreplay')) {
|
|
98
|
+
p.plugins.push({ id: 'app.nilvn.abreplay' });
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// v8 -> v9: keyframe animation became the `animstudio` plugin, same policy as
|
|
103
|
+
// v7→v8 — usage is "any animation node in any scene".
|
|
104
|
+
{
|
|
105
|
+
to: 9,
|
|
106
|
+
run: (p, ctx) => {
|
|
107
|
+
if (hasPlugin(p, 'app.nilvn.animstudio'))
|
|
108
|
+
return;
|
|
109
|
+
const animKinds = new Set(['anim', 'eventframe', 'loopstart', 'loopstop']);
|
|
110
|
+
const used = p.scenes.some((s) => s.nodes.some((n) => animKinds.has(n.kind)));
|
|
111
|
+
if (used || ctx.hadPlugins)
|
|
112
|
+
p.plugins.push({ id: 'app.nilvn.animstudio' });
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// v9 -> v10: the per-line voice authoring UI became the editor-only
|
|
116
|
+
// `voicerecord` plugin, same policy again. Only the popover is gated —
|
|
117
|
+
// [voice] playback stays core — so the backfill is about keeping the voice
|
|
118
|
+
// button visible where the author was using or could see it.
|
|
119
|
+
{
|
|
120
|
+
to: 10,
|
|
121
|
+
run: (p, ctx) => {
|
|
122
|
+
if (hasPlugin(p, 'app.nilvn.voicerecord'))
|
|
123
|
+
return;
|
|
124
|
+
const used = p.scenes.some((s) => s.nodes.some((n) => n.kind === 'say' && !!n.voice));
|
|
125
|
+
if (used || ctx.hadPlugins)
|
|
126
|
+
p.plugins.push({ id: 'app.nilvn.voicerecord' });
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// v10 -> v11: plugin platform v2. Refs are keyed by reverse-DNS id; a dotless
|
|
130
|
+
// (short) name maps into the first-party namespace through resolvePluginId —
|
|
131
|
+
// a first-party short name becomes its id, anything else a valid id the
|
|
132
|
+
// editor's load-time validation reports as unknown (data is never dropped);
|
|
133
|
+
// an id passes verbatim, `entry` is gone (entries live in the manifest),
|
|
134
|
+
// duplicates collapse.
|
|
135
|
+
{
|
|
136
|
+
to: 11,
|
|
137
|
+
run: (p) => {
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
const next = [];
|
|
140
|
+
for (const raw of p.plugins) {
|
|
141
|
+
const id = refId(raw);
|
|
142
|
+
if (!id || seen.has(id))
|
|
143
|
+
continue;
|
|
144
|
+
seen.add(id);
|
|
145
|
+
const ref = { id };
|
|
146
|
+
if (raw.version !== undefined)
|
|
147
|
+
ref.version = raw.version;
|
|
148
|
+
if (raw.config !== undefined)
|
|
149
|
+
ref.config = raw.config;
|
|
150
|
+
next.push(ref);
|
|
151
|
+
}
|
|
152
|
+
p.plugins = next;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
/** Bring a loaded project up to CURRENT_SCHEMA_VERSION in place (then return it).
|
|
157
|
+
* Call once on load, after reading from disk. Runs every table step the project
|
|
158
|
+
* predates, in order, then stamps the current version. */
|
|
159
|
+
export function migrateProject(project) {
|
|
160
|
+
const from = project.meta.schemaVersion ?? 1;
|
|
161
|
+
const ctx = { hadPlugins: project.plugins.length > 0 };
|
|
162
|
+
for (const m of MIGRATIONS)
|
|
163
|
+
if (from < m.to)
|
|
164
|
+
m.run(project, ctx);
|
|
165
|
+
project.meta.schemaVersion = CURRENT_SCHEMA_VERSION;
|
|
166
|
+
return project;
|
|
167
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Project } from './ir.js';
|
|
2
|
+
import { type ChunkManifest, type ManifestAsset } from './chunk.js';
|
|
3
|
+
import { type BuildChunkedOptions, type ChunkFile } from './chunk-build.js';
|
|
4
|
+
/** Script-package format version. Bumped only when nilvn.json's shape changes
|
|
5
|
+
* incompatibly; the embedded chunk manifest keeps its own `format`. */
|
|
6
|
+
export declare const PACKAGE_FORMAT = 1;
|
|
7
|
+
/** The package's single entry file, at the package root. */
|
|
8
|
+
export declare const PACKAGE_MANIFEST_FILE = "nilvn.json";
|
|
9
|
+
/** An actor as the runtime needs it (mirrors the engine's `ActorDef`; core
|
|
10
|
+
* cannot import the engine). `name` is the default-language literal (fallback),
|
|
11
|
+
* `nameKey` lets the engine re-resolve the display name on a language switch. */
|
|
12
|
+
export interface PackageActor {
|
|
13
|
+
name?: string;
|
|
14
|
+
nameKey?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
/** Sprite URL template; `{face}` is replaced by the current face. */
|
|
17
|
+
sprites?: string;
|
|
18
|
+
defaultFace?: string;
|
|
19
|
+
/** Base pitch (Hz) for the voice blip. */
|
|
20
|
+
voice?: number;
|
|
21
|
+
}
|
|
22
|
+
/** One enabled plugin, by its reverse-DNS id (`app.nilvn.textfx`; the engine
|
|
23
|
+
* also accepts the short names older packages carry).
|
|
24
|
+
* `entry` = a plugin carried inside the package (its `plugin.json`, relative to
|
|
25
|
+
* the package root); `integrity` = its digest (reserved). */
|
|
26
|
+
export interface PackagePlugin {
|
|
27
|
+
id: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
entry?: string;
|
|
30
|
+
integrity?: string;
|
|
31
|
+
}
|
|
32
|
+
/** `nilvn.json` — the package manifest. */
|
|
33
|
+
export interface PackageManifest {
|
|
34
|
+
/** = PACKAGE_FORMAT. */
|
|
35
|
+
format: number;
|
|
36
|
+
/** Work title (window / document title). */
|
|
37
|
+
title: string;
|
|
38
|
+
/** The studio / engine version that produced the package — traceability only;
|
|
39
|
+
* compatibility is decided by `format` + `chunks.format` + `chunks.schemaVersion`. */
|
|
40
|
+
engine: string;
|
|
41
|
+
/** Initial content language (the author's language). */
|
|
42
|
+
lang: string;
|
|
43
|
+
/** Languages the in-game switcher offers (the default first). */
|
|
44
|
+
languages: string[];
|
|
45
|
+
actors: Record<string, PackageActor>;
|
|
46
|
+
/** The enabled plugin set, auto-loaded at start (same entries as `[use …]`). */
|
|
47
|
+
plugins: PackagePlugin[];
|
|
48
|
+
/** Typewriter speed, characters per second. */
|
|
49
|
+
textSpeed: number;
|
|
50
|
+
/** Per-work id the runtime namespaces saves / settings by. */
|
|
51
|
+
saveKey: string;
|
|
52
|
+
/** The chunk manifest (chunk.ts), embedded as-is: chunk / locale-slice / asset
|
|
53
|
+
* index + entry label. Wire files (`chunks/**`, `assets/**`) are unchanged. */
|
|
54
|
+
chunks: ChunkManifest;
|
|
55
|
+
}
|
|
56
|
+
/** Structural check for a parsed nilvn.json — a foreign or truncated file fails
|
|
57
|
+
* cleanly here instead of deep inside playback. Does NOT check `format`
|
|
58
|
+
* numbers (callers gate those so they can word the mismatch message). */
|
|
59
|
+
export declare function isPackageManifest(x: unknown): x is PackageManifest;
|
|
60
|
+
export interface BuildPackageOptions extends BuildChunkedOptions {
|
|
61
|
+
/** Defaults to the project title (or "NilVN"). */
|
|
62
|
+
title?: string;
|
|
63
|
+
/** Defaults to the project's default language. */
|
|
64
|
+
lang?: string;
|
|
65
|
+
/** Defaults to {@link packageLanguages}. */
|
|
66
|
+
languages?: string[];
|
|
67
|
+
/** Defaults to {@link packageActors} (the IR actors as-is; the editor passes
|
|
68
|
+
* its own table so actors without artwork get a placeholder sprite). */
|
|
69
|
+
actors?: Record<string, PackageActor>;
|
|
70
|
+
/** Defaults to the project's enabled plugin ids. The editor appends the
|
|
71
|
+
* finished-game shell (`app.nilvn.menu`) and drops plugins without an engine half. */
|
|
72
|
+
plugins?: PackagePlugin[];
|
|
73
|
+
/** Defaults to the project text speed (40). */
|
|
74
|
+
textSpeed?: number;
|
|
75
|
+
/** Defaults to the project id, else the title. */
|
|
76
|
+
saveKey?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface ScriptPackagePlan {
|
|
79
|
+
/** nilvn.json, with `chunks.assets` still EMPTY — the producer resolves the
|
|
80
|
+
* refs to files and fills it via {@link fillPackageAssets}. */
|
|
81
|
+
manifest: PackageManifest;
|
|
82
|
+
/** chunks/meta.json + chunks/scene/*.json + chunks/locale/<lang>/*.json. */
|
|
83
|
+
files: ChunkFile[];
|
|
84
|
+
/** Deduped asset refs the scenes reference (actor faces are NOT here — they
|
|
85
|
+
* belong to the whole-project universe the producer resolves separately). */
|
|
86
|
+
assetRefs: string[];
|
|
87
|
+
}
|
|
88
|
+
/** Languages the finished work can switch between: the default plus every
|
|
89
|
+
* declared language that ships a non-empty catalog. The default comes first. */
|
|
90
|
+
export declare function packageLanguages(project: Project): string[];
|
|
91
|
+
/** The runtime actor table straight from the IR (`name` = the default-language
|
|
92
|
+
* display name, falling back to the id). */
|
|
93
|
+
export declare function packageActors(project: Project): Record<string, PackageActor>;
|
|
94
|
+
/** Build a script package from a project: the chunked script side (one chunk per
|
|
95
|
+
* scene by default; `groups` merges scenes — a single group = one chunk, the
|
|
96
|
+
* single-file / asset-ZIP shape) plus the manifest fields the shells used to bake
|
|
97
|
+
* into their bootstraps. Pure, zero-I/O. */
|
|
98
|
+
export declare function buildScriptPackage(project: Project, opts: BuildPackageOptions): ScriptPackagePlan;
|
|
99
|
+
/** Fill the by-ref asset table (`chunks.assets`). Returns a new manifest. */
|
|
100
|
+
export declare function fillPackageAssets(manifest: PackageManifest, assets: Record<string, ManifestAsset>): PackageManifest;
|