@doodle-engine/cli 0.0.8 → 0.0.10

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/cli.js CHANGED
@@ -1,358 +1,258 @@
1
1
  #!/usr/bin/env node
2
- import { Command as I } from "commander";
3
- import { crayon as i } from "crayon.js";
4
- import { createServer as x, build as G } from "vite";
5
- import $ from "@vitejs/plugin-react";
6
- import { watch as R } from "chokidar";
7
- import { readdir as y, readFile as f, mkdir as j, writeFile as h } from "fs/promises";
8
- import { join as s, extname as p, relative as w } from "path";
2
+ import { Command as G } from "commander";
3
+ import { crayon as c } from "crayon.js";
4
+ import { createServer as j, build as $ } from "vite";
5
+ import A from "@vitejs/plugin-react";
6
+ import { watch as H } from "chokidar";
7
+ import { readdir as y, readFile as p, mkdir as x, writeFile as m } from "fs/promises";
8
+ import { join as s, extname as f, relative as w } from "path";
9
9
  import { parse as b } from "yaml";
10
- import { parseDialogue as E } from "@doodle-engine/core";
11
- import H from "prompts";
12
- function N(e, o) {
13
- const a = [];
14
- for (const n of Object.values(e.dialogues)) {
15
- const r = o.get(n.id) || `dialogue:${n.id}`;
16
- a.push(...M(n, r));
10
+ import { parseDialogue as v } from "@doodle-engine/core";
11
+ import M from "prompts";
12
+ function I(e, n) {
13
+ const i = [];
14
+ for (const a of Object.values(e.dialogues)) {
15
+ const o = n.get(a.id) || `dialogue:${a.id}`;
16
+ i.push(...F(a, o));
17
17
  }
18
- for (const n of Object.values(e.characters))
19
- if (n.dialogue && !e.dialogues[n.dialogue]) {
20
- const r = o.get(n.id) || `character:${n.id}`;
21
- a.push({
22
- file: r,
23
- message: `Character "${n.id}" references non-existent dialogue "${n.dialogue}"`,
24
- suggestion: `Create dialogue "${n.dialogue}" or fix the reference`
18
+ for (const a of Object.values(e.characters))
19
+ if (a.dialogue && !e.dialogues[a.dialogue]) {
20
+ const o = n.get(a.id) || `character:${a.id}`;
21
+ i.push({
22
+ file: o,
23
+ message: `Character "${a.id}" references non-existent dialogue "${a.dialogue}"`,
24
+ suggestion: `Create dialogue "${a.dialogue}" or fix the reference`
25
25
  });
26
26
  }
27
- return a.push(...B(e, o)), a;
27
+ return i.push(...J(e, n)), i;
28
28
  }
29
- function M(e, o) {
30
- const a = [], n = /* @__PURE__ */ new Set();
31
- for (const r of e.nodes)
32
- n.has(r.id) && a.push({
33
- file: o,
34
- message: `Duplicate node ID "${r.id}"`,
29
+ function F(e, n) {
30
+ const i = [], a = /* @__PURE__ */ new Set();
31
+ for (const o of e.nodes)
32
+ a.has(o.id) && i.push({
33
+ file: n,
34
+ message: `Duplicate node ID "${o.id}"`,
35
35
  suggestion: "Node IDs must be unique within a dialogue"
36
- }), n.add(r.id);
37
- n.has(e.startNode) || a.push({
38
- file: o,
36
+ }), a.add(o.id);
37
+ a.has(e.startNode) || i.push({
38
+ file: n,
39
39
  message: `Start node "${e.startNode}" not found`,
40
40
  suggestion: `Add a NODE ${e.startNode} or fix the startNode reference`
41
41
  });
42
- for (const r of e.nodes)
43
- a.push(...F(r, n, o));
44
- return a;
42
+ for (const o of e.nodes)
43
+ i.push(...B(o, a, n));
44
+ return i;
45
45
  }
46
- function F(e, o, a) {
47
- const n = [];
48
- if (e.next && !o.has(e.next) && n.push({
49
- file: a,
46
+ function B(e, n, i) {
47
+ const a = [];
48
+ if (e.next && !n.has(e.next) && a.push({
49
+ file: i,
50
50
  message: `Node "${e.id}" GOTO "${e.next}" points to non-existent node`,
51
51
  suggestion: `Add NODE ${e.next} or fix the GOTO target`
52
52
  }), e.conditionalNext)
53
- for (const r of e.conditionalNext)
54
- o.has(r.next) || n.push({
55
- file: a,
56
- message: `Node "${e.id}" IF block GOTO "${r.next}" points to non-existent node`,
57
- suggestion: `Add NODE ${r.next} or fix the GOTO target`
58
- }), n.push(...v(r.condition, e.id, a));
59
- for (const r of e.choices) {
60
- if (o.has(r.next) || n.push({
61
- file: a,
62
- message: `Node "${e.id}" choice "${r.id}" GOTO "${r.next}" points to non-existent node`,
63
- suggestion: `Add NODE ${r.next} or fix the GOTO target`
64
- }), r.conditions)
65
- for (const c of r.conditions)
66
- n.push(...v(c, e.id, a));
67
- if (r.effects)
68
- for (const c of r.effects)
69
- n.push(...T(c, e.id, a));
53
+ for (const o of e.conditionalNext)
54
+ n.has(o.next) || a.push({
55
+ file: i,
56
+ message: `Node "${e.id}" IF block GOTO "${o.next}" points to non-existent node`,
57
+ suggestion: `Add NODE ${o.next} or fix the GOTO target`
58
+ }), a.push(...O(o.condition, e.id, i));
59
+ for (const o of e.choices) {
60
+ if (!o.effects?.some(
61
+ (t) => t.type === "endDialogue" || t.type === "goToLocation"
62
+ ) && !n.has(o.next) && a.push({
63
+ file: i,
64
+ message: `Node "${e.id}" choice "${o.id}" GOTO "${o.next}" points to non-existent node`,
65
+ suggestion: `Add NODE ${o.next} or fix the GOTO target`
66
+ }), o.conditions)
67
+ for (const t of o.conditions)
68
+ a.push(...O(t, e.id, i));
69
+ if (o.effects)
70
+ for (const t of o.effects)
71
+ a.push(...C(t, e.id, i));
70
72
  }
71
73
  if (e.conditions)
72
- for (const r of e.conditions)
73
- n.push(...v(r, e.id, a));
74
+ for (const o of e.conditions)
75
+ a.push(...O(o, e.id, i));
74
76
  if (e.effects)
75
- for (const r of e.effects)
76
- n.push(...T(r, e.id, a));
77
- return n;
77
+ for (const o of e.effects)
78
+ a.push(...C(o, e.id, i));
79
+ return a;
78
80
  }
79
- function v(e, o, a) {
80
- const n = [];
81
+ const L = {
82
+ hasFlag: ["flag"],
83
+ notFlag: ["flag"],
84
+ hasItem: ["itemId"],
85
+ notItem: ["itemId"],
86
+ variableEquals: ["variable", "value"],
87
+ variableGreaterThan: ["variable", "value"],
88
+ variableLessThan: ["variable", "value"],
89
+ questAtStage: ["questId", "stageId"],
90
+ atLocation: ["locationId"],
91
+ characterAt: ["characterId", "locationId"],
92
+ characterInParty: ["characterId"],
93
+ relationshipAbove: ["characterId", "value"],
94
+ relationshipBelow: ["characterId", "value"],
95
+ itemAt: ["itemId", "locationId"]
96
+ }, U = {
97
+ setFlag: ["flag"],
98
+ clearFlag: ["flag"],
99
+ setVariable: ["variable", "value"],
100
+ addVariable: ["variable", "value"],
101
+ addItem: ["itemId"],
102
+ removeItem: ["itemId"],
103
+ moveItem: ["itemId", "locationId"],
104
+ goToLocation: ["locationId"],
105
+ advanceTime: ["hours"],
106
+ setQuestStage: ["questId", "stageId"],
107
+ addJournalEntry: ["entryId"],
108
+ startDialogue: ["dialogueId"],
109
+ endDialogue: [],
110
+ setCharacterLocation: ["characterId", "locationId"],
111
+ addToParty: ["characterId"],
112
+ removeFromParty: ["characterId"],
113
+ setRelationship: ["characterId", "value"],
114
+ addRelationship: ["characterId", "value"],
115
+ setCharacterStat: ["characterId", "stat", "value"],
116
+ addCharacterStat: ["characterId", "stat", "value"],
117
+ setMapEnabled: ["enabled"],
118
+ playMusic: ["track"],
119
+ playSound: ["sound"],
120
+ notify: ["message"],
121
+ playVideo: ["file"],
122
+ showInterlude: ["interludeId"]
123
+ };
124
+ function O(e, n, i) {
125
+ const a = [];
81
126
  if (!e.type)
82
- return n.push({
83
- file: a,
84
- message: `Node "${o}" has condition with missing type`
85
- }), n;
86
- switch (e.type) {
87
- case "hasFlag":
88
- case "notFlag":
89
- e.flag || n.push({
90
- file: a,
91
- message: `Node "${o}" condition "${e.type}" missing required "flag" argument`
92
- });
93
- break;
94
- case "hasItem":
95
- case "notItem":
96
- e.item || n.push({
97
- file: a,
98
- message: `Node "${o}" condition "${e.type}" missing required "item" argument`
99
- });
100
- break;
101
- case "questAtStage":
102
- e.quest || n.push({
103
- file: a,
104
- message: `Node "${o}" condition "questAtStage" missing required "quest" argument`
105
- }), e.stage || n.push({
106
- file: a,
107
- message: `Node "${o}" condition "questAtStage" missing required "stage" argument`
108
- });
109
- break;
110
- case "variableEquals":
111
- case "variableGreaterThan":
112
- case "variableLessThan":
113
- e.variable || n.push({
114
- file: a,
115
- message: `Node "${o}" condition "${e.type}" missing required "variable" argument`
116
- }), e.value === void 0 && n.push({
117
- file: a,
118
- message: `Node "${o}" condition "${e.type}" missing required "value" argument`
119
- });
120
- break;
121
- }
122
- return n;
127
+ return a.push({
128
+ file: i,
129
+ message: `Node "${n}" has condition with missing type`
130
+ }), a;
131
+ if (e.type === "timeIs")
132
+ return e.hour === void 0 && e.day === void 0 && a.push({
133
+ file: i,
134
+ message: `Node "${n}" condition "timeIs" must have at least one of "hour" or "day" argument`
135
+ }), a;
136
+ const o = L[e.type];
137
+ if (!o)
138
+ return a;
139
+ for (const r of o)
140
+ (e[r] === void 0 || e[r] === null || e[r] === "") && a.push({
141
+ file: i,
142
+ message: `Node "${n}" condition "${e.type}" missing required "${r}" argument`
143
+ });
144
+ return a;
123
145
  }
124
- function T(e, o, a) {
125
- const n = [];
146
+ function C(e, n, i) {
147
+ const a = [];
126
148
  if (!e.type)
127
- return n.push({
128
- file: a,
129
- message: `Node "${o}" has effect with missing type`
130
- }), n;
131
- switch (e.type) {
132
- case "setFlag":
133
- case "clearFlag":
134
- e.flag || n.push({
135
- file: a,
136
- message: `Node "${o}" effect "${e.type}" missing required "flag" argument`
137
- });
138
- break;
139
- case "setVariable":
140
- case "addVariable":
141
- e.variable || n.push({
142
- file: a,
143
- message: `Node "${o}" effect "${e.type}" missing required "variable" argument`
144
- }), e.value === void 0 && n.push({
145
- file: a,
146
- message: `Node "${o}" effect "${e.type}" missing required "value" argument`
147
- });
148
- break;
149
- case "addItem":
150
- case "removeItem":
151
- e.item || n.push({
152
- file: a,
153
- message: `Node "${o}" effect "${e.type}" missing required "item" argument`
154
- });
155
- break;
156
- case "moveItem":
157
- e.item || n.push({
158
- file: a,
159
- message: `Node "${o}" effect "moveItem" missing required "item" argument`
160
- }), e.location || n.push({
161
- file: a,
162
- message: `Node "${o}" effect "moveItem" missing required "location" argument`
163
- });
164
- break;
165
- case "setQuestStage":
166
- e.quest || n.push({
167
- file: a,
168
- message: `Node "${o}" effect "setQuestStage" missing required "quest" argument`
169
- }), e.stage || n.push({
170
- file: a,
171
- message: `Node "${o}" effect "setQuestStage" missing required "stage" argument`
172
- });
173
- break;
174
- case "addJournalEntry":
175
- e.entry || n.push({
176
- file: a,
177
- message: `Node "${o}" effect "addJournalEntry" missing required "entry" argument`
178
- });
179
- break;
180
- case "setCharacterLocation":
181
- e.character || n.push({
182
- file: a,
183
- message: `Node "${o}" effect "setCharacterLocation" missing required "character" argument`
184
- }), e.location || n.push({
185
- file: a,
186
- message: `Node "${o}" effect "setCharacterLocation" missing required "location" argument`
187
- });
188
- break;
189
- case "addToParty":
190
- case "removeFromParty":
191
- e.character || n.push({
192
- file: a,
193
- message: `Node "${o}" effect "${e.type}" missing required "character" argument`
194
- });
195
- break;
196
- case "setRelationship":
197
- case "addRelationship":
198
- e.character || n.push({
199
- file: a,
200
- message: `Node "${o}" effect "${e.type}" missing required "character" argument`
201
- }), e.value === void 0 && n.push({
202
- file: a,
203
- message: `Node "${o}" effect "${e.type}" missing required "value" argument`
204
- });
205
- break;
206
- case "setCharacterStat":
207
- case "addCharacterStat":
208
- e.character || n.push({
209
- file: a,
210
- message: `Node "${o}" effect "${e.type}" missing required "character" argument`
211
- }), e.stat || n.push({
212
- file: a,
213
- message: `Node "${o}" effect "${e.type}" missing required "stat" argument`
214
- }), e.value === void 0 && n.push({
215
- file: a,
216
- message: `Node "${o}" effect "${e.type}" missing required "value" argument`
217
- });
218
- break;
219
- case "setMapEnabled":
220
- e.enabled === void 0 && n.push({
221
- file: a,
222
- message: `Node "${o}" effect "setMapEnabled" missing required "enabled" argument`
223
- });
224
- break;
225
- case "advanceTime":
226
- e.hours === void 0 && n.push({
227
- file: a,
228
- message: `Node "${o}" effect "advanceTime" missing required "hours" argument`
229
- });
230
- break;
231
- case "goToLocation":
232
- e.location || n.push({
233
- file: a,
234
- message: `Node "${o}" effect "goToLocation" missing required "location" argument`
235
- });
236
- break;
237
- case "startDialogue":
238
- e.dialogue || n.push({
239
- file: a,
240
- message: `Node "${o}" effect "startDialogue" missing required "dialogue" argument`
241
- });
242
- break;
243
- case "playMusic":
244
- case "playSound":
245
- e.file || n.push({
246
- file: a,
247
- message: `Node "${o}" effect "${e.type}" missing required "file" argument`
248
- });
249
- break;
250
- case "playVideo":
251
- e.file || n.push({
252
- file: a,
253
- message: `Node "${o}" effect "playVideo" missing required "file" argument`
254
- });
255
- break;
256
- case "notify":
257
- e.message || n.push({
258
- file: a,
259
- message: `Node "${o}" effect "notify" missing required "message" argument`
260
- });
261
- break;
262
- }
263
- return n;
149
+ return a.push({
150
+ file: i,
151
+ message: `Node "${n}" has effect with missing type`
152
+ }), a;
153
+ const o = U[e.type];
154
+ if (!o)
155
+ return a;
156
+ for (const r of o)
157
+ (e[r] === void 0 || e[r] === null || e[r] === "") && a.push({
158
+ file: i,
159
+ message: `Node "${n}" effect "${e.type}" missing required "${r}" argument`
160
+ });
161
+ return a;
264
162
  }
265
- function B(e, o) {
266
- const a = [], n = /* @__PURE__ */ new Set();
163
+ function J(e, n) {
164
+ const i = [], a = /* @__PURE__ */ new Set();
267
165
  for (const t of Object.values(e.locales))
268
166
  for (const l of Object.keys(t))
269
- n.add(l);
270
- const r = (t) => t.startsWith("@"), c = (t, l, d) => {
271
- const m = t.slice(1);
272
- if (!n.has(m)) {
273
- const u = o.get(l) || `${d}:${l}`;
274
- a.push({
167
+ a.add(l);
168
+ const o = (t) => t.startsWith("@"), r = (t, l, d) => {
169
+ const h = t.slice(1);
170
+ if (!a.has(h)) {
171
+ const u = n.get(l) || `${d}:${l}`;
172
+ i.push({
275
173
  file: u,
276
174
  message: `Localization key "${t}" not found in any locale file`,
277
- suggestion: `Add "${m}: ..." to your locale files`
175
+ suggestion: `Add "${h}: ..." to your locale files`
278
176
  });
279
177
  }
280
178
  };
281
179
  for (const t of Object.values(e.locations))
282
- r(t.name) && c(t.name, t.id, "location"), r(t.description) && c(t.description, t.id, "location");
180
+ o(t.name) && r(t.name, t.id, "location"), o(t.description) && r(t.description, t.id, "location");
283
181
  for (const t of Object.values(e.characters))
284
- r(t.name) && c(t.name, t.id, "character"), r(t.biography) && c(t.biography, t.id, "character");
182
+ o(t.name) && r(t.name, t.id, "character"), o(t.biography) && r(t.biography, t.id, "character");
285
183
  for (const t of Object.values(e.items))
286
- r(t.name) && c(t.name, t.id, "item"), r(t.description) && c(t.description, t.id, "item");
184
+ o(t.name) && r(t.name, t.id, "item"), o(t.description) && r(t.description, t.id, "item");
287
185
  for (const t of Object.values(e.quests)) {
288
- r(t.name) && c(t.name, t.id, "quest"), r(t.description) && c(t.description, t.id, "quest");
186
+ o(t.name) && r(t.name, t.id, "quest"), o(t.description) && r(t.description, t.id, "quest");
289
187
  for (const l of t.stages)
290
- r(l.description) && c(l.description, t.id, "quest");
188
+ o(l.description) && r(l.description, t.id, "quest");
291
189
  }
292
190
  for (const t of Object.values(e.journalEntries))
293
- r(t.title) && c(t.title, t.id, "journal"), r(t.text) && c(t.text, t.id, "journal");
191
+ o(t.title) && r(t.title, t.id, "journal"), o(t.text) && r(t.text, t.id, "journal");
294
192
  for (const t of Object.values(e.dialogues))
295
193
  for (const l of t.nodes) {
296
- r(l.text) && c(l.text, t.id, "dialogue");
194
+ o(l.text) && r(l.text, t.id, "dialogue");
297
195
  for (const d of l.choices)
298
- r(d.text) && c(d.text, t.id, "dialogue");
196
+ o(d.text) && r(d.text, t.id, "dialogue");
299
197
  }
300
- return a;
198
+ for (const t of Object.values(e.interludes))
199
+ o(t.text) && r(t.text, t.id, "interlude");
200
+ return i;
301
201
  }
302
- function D(e) {
202
+ function T(e) {
303
203
  if (e.length === 0) {
304
- console.log(i.green("✓ No validation errors"));
204
+ console.log(c.green("✓ No validation errors"));
305
205
  return;
306
206
  }
307
- console.log(i.red(`
207
+ console.log(c.red(`
308
208
  ✗ Found ${e.length} validation error${e.length === 1 ? "" : "s"}:
309
209
  `));
310
- for (const o of e)
311
- console.log(i.bold(o.file) + (o.line ? `:${o.line}` : "")), console.log(" " + i.red(o.message)), o.suggestion && console.log(" " + i.dim(o.suggestion)), console.log();
210
+ for (const n of e)
211
+ console.log(c.bold(n.file) + (n.line ? `:${n.line}` : "")), console.log(" " + c.red(n.message)), n.suggestion && console.log(" " + c.dim(n.suggestion)), console.log();
312
212
  }
313
- const C = "🐾", L = "✨", J = "✏️", U = "➕";
314
- async function W() {
315
- const e = process.cwd(), o = s(e, "content");
316
- console.log(""), console.log(i.bold.magenta(` ${C} Doodle Engine Dev Server ${C}`)), console.log("");
317
- const a = {
213
+ const N = "🐾", W = "✨", Q = "✏️", V = "➕";
214
+ async function Y() {
215
+ const e = process.cwd(), n = s(e, "content");
216
+ console.log(""), console.log(c.bold.magenta(` ${N} Doodle Engine Dev Server ${N}`)), console.log("");
217
+ const i = {
318
218
  name: "doodle-content-loader",
319
- configureServer(r) {
320
- r.middlewares.use("/api/content", async (t, l) => {
219
+ configureServer(o) {
220
+ o.middlewares.use("/api/content", async (t, l) => {
321
221
  try {
322
- const d = await Q(o);
222
+ const d = await P(n);
323
223
  l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(d));
324
224
  } catch (d) {
325
- console.error(i.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
225
+ console.error(c.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
326
226
  }
327
227
  });
328
- const c = R(s(o, "**/*"), {
228
+ const r = H(n, {
329
229
  ignored: /(^|[\/\\])\../,
330
230
  persistent: !0
331
231
  });
332
- c.on("change", async (t) => {
333
- console.log(i.yellow(` ${J} Content changed: ${t}`)), await q(o), r.ws.send({
232
+ r.on("change", async (t) => {
233
+ console.log(c.yellow(` ${Q} Content changed: ${t}`)), await S(n), o.ws.send({
334
234
  type: "full-reload",
335
235
  path: "*"
336
236
  });
337
- }), c.on("add", async (t) => {
338
- console.log(i.green(` ${U} Content added: ${t}`)), await q(o), r.ws.send({
237
+ }), r.on("add", async (t) => {
238
+ console.log(c.green(` ${V} Content added: ${t}`)), await S(n), o.ws.send({
339
239
  type: "full-reload",
340
240
  path: "*"
341
241
  });
342
242
  });
343
243
  }
344
- }, n = await x({
244
+ }, a = await j({
345
245
  root: e,
346
- plugins: [$(), a],
246
+ plugins: [A(), i],
347
247
  server: {
348
248
  port: 3e3,
349
249
  open: !0
350
250
  }
351
251
  });
352
- await n.listen(), n.printUrls(), console.log(""), console.log(i.dim(` ${L} Watching content files for changes...`)), console.log("");
252
+ await a.listen(), a.printUrls(), console.log(""), console.log(c.dim(` ${W} Watching content files for changes...`)), console.log("");
353
253
  }
354
- async function Q(e) {
355
- const o = {
254
+ async function P(e) {
255
+ const n = {
356
256
  locations: {},
357
257
  characters: {},
358
258
  items: {},
@@ -360,52 +260,54 @@ async function Q(e) {
360
260
  dialogues: {},
361
261
  quests: {},
362
262
  journalEntries: {},
263
+ interludes: {},
363
264
  locales: {}
364
265
  };
365
- let a = null;
366
- const n = [
266
+ let i = null;
267
+ const a = [
367
268
  { dir: "locations", key: "locations" },
368
269
  { dir: "characters", key: "characters" },
369
270
  { dir: "items", key: "items" },
370
271
  { dir: "maps", key: "maps" },
371
272
  { dir: "quests", key: "quests" },
372
- { dir: "journal", key: "journalEntries" }
273
+ { dir: "journal", key: "journalEntries" },
274
+ { dir: "interludes", key: "interludes" }
373
275
  ];
374
- for (const { dir: r, key: c } of n) {
375
- const t = s(e, r);
276
+ for (const { dir: o, key: r } of a) {
277
+ const t = s(e, o);
376
278
  try {
377
279
  const l = await y(t);
378
280
  for (const d of l)
379
- if (p(d) === ".yaml" || p(d) === ".yml") {
380
- const m = s(t, d), u = await f(m, "utf-8"), g = b(u);
381
- g && g.id && (o[c][g.id] = g);
281
+ if (f(d) === ".yaml" || f(d) === ".yml") {
282
+ const h = s(t, d), u = await p(h, "utf-8"), g = b(u);
283
+ g && g.id && (n[r][g.id] = g);
382
284
  }
383
285
  } catch {
384
286
  }
385
287
  }
386
288
  try {
387
- const r = s(e, "locales"), c = await y(r);
388
- for (const t of c)
389
- if (p(t) === ".yaml" || p(t) === ".yml") {
390
- const l = s(r, t), d = await f(l, "utf-8"), m = b(d), u = t.replace(/\.(yaml|yml)$/, "");
391
- o.locales[u] = m ?? {};
289
+ const o = s(e, "locales"), r = await y(o);
290
+ for (const t of r)
291
+ if (f(t) === ".yaml" || f(t) === ".yml") {
292
+ const l = s(o, t), d = await p(l, "utf-8"), h = b(d), u = t.replace(/\.(yaml|yml)$/, "");
293
+ n.locales[u] = h ?? {};
392
294
  }
393
295
  } catch {
394
296
  }
395
297
  try {
396
- const r = s(e, "dialogues"), c = await y(r);
397
- for (const t of c)
398
- if (p(t) === ".dlg") {
399
- const l = s(r, t), d = await f(l, "utf-8"), m = t.replace(".dlg", ""), u = E(d, m);
400
- o.dialogues[u.id] = u;
298
+ const o = s(e, "dialogues"), r = await y(o);
299
+ for (const t of r)
300
+ if (f(t) === ".dlg") {
301
+ const l = s(o, t), d = await p(l, "utf-8"), h = t.replace(".dlg", ""), u = v(d, h);
302
+ n.dialogues[u.id] = u;
401
303
  }
402
304
  } catch {
403
305
  }
404
306
  try {
405
- const r = s(e, "game.yaml"), c = await f(r, "utf-8");
406
- a = b(c);
307
+ const o = s(e, "game.yaml"), r = await p(o, "utf-8");
308
+ i = b(r);
407
309
  } catch {
408
- console.warn(i.yellow(" No game.yaml found, using defaults")), a = {
310
+ console.warn(c.yellow(" No game.yaml found, using defaults")), i = {
409
311
  startLocation: "tavern",
410
312
  startTime: { day: 1, hour: 8 },
411
313
  startFlags: {},
@@ -413,18 +315,18 @@ async function Q(e) {
413
315
  startInventory: []
414
316
  };
415
317
  }
416
- return { registry: o, config: a };
318
+ return { registry: n, config: i };
417
319
  }
418
- async function q(e) {
320
+ async function S(e) {
419
321
  try {
420
- const { registry: o, fileMap: a } = await V(e), n = N(o, a);
421
- n.length > 0 && (console.log(""), D(n), console.log(""));
422
- } catch (o) {
423
- console.error(i.red(" Error running validation:"), o);
322
+ const { registry: n, fileMap: i } = await K(e), a = I(n, i);
323
+ a.length > 0 && (console.log(""), T(a), console.log(""));
324
+ } catch (n) {
325
+ console.error(c.red(" Error running validation:"), n);
424
326
  }
425
327
  }
426
- async function V(e) {
427
- const o = {
328
+ async function K(e) {
329
+ const n = {
428
330
  locations: {},
429
331
  characters: {},
430
332
  items: {},
@@ -432,72 +334,77 @@ async function V(e) {
432
334
  dialogues: {},
433
335
  quests: {},
434
336
  journalEntries: {},
337
+ interludes: {},
435
338
  locales: {}
436
- }, a = /* @__PURE__ */ new Map(), n = [
339
+ }, i = /* @__PURE__ */ new Map(), a = [
437
340
  { dir: "locations", key: "locations" },
438
341
  { dir: "characters", key: "characters" },
439
342
  { dir: "items", key: "items" },
440
343
  { dir: "maps", key: "maps" },
441
344
  { dir: "quests", key: "quests" },
442
- { dir: "journal", key: "journalEntries" }
345
+ { dir: "journal", key: "journalEntries" },
346
+ { dir: "interludes", key: "interludes" }
443
347
  ];
444
- for (const { dir: r, key: c } of n) {
445
- const t = s(e, r);
348
+ for (const { dir: o, key: r } of a) {
349
+ const t = s(e, o);
446
350
  try {
447
351
  const l = await y(t);
448
352
  for (const d of l)
449
- if (p(d) === ".yaml" || p(d) === ".yml") {
450
- const m = s(t, d), u = await f(m, "utf-8"), g = b(u);
451
- g && g.id && (o[c][g.id] = g, a.set(g.id, w(process.cwd(), m)));
353
+ if (f(d) === ".yaml" || f(d) === ".yml") {
354
+ const h = s(t, d), u = await p(h, "utf-8"), g = b(u);
355
+ g && g.id && (n[r][g.id] = g, i.set(g.id, w(process.cwd(), h)));
452
356
  }
453
357
  } catch {
454
358
  }
455
359
  }
456
360
  try {
457
- const r = s(e, "locales"), c = await y(r);
458
- for (const t of c)
459
- if (p(t) === ".yaml" || p(t) === ".yml") {
460
- const l = s(r, t), d = await f(l, "utf-8"), m = b(d), u = t.replace(/\.(yaml|yml)$/, "");
461
- o.locales[u] = m ?? {};
361
+ const o = s(e, "locales"), r = await y(o);
362
+ for (const t of r)
363
+ if (f(t) === ".yaml" || f(t) === ".yml") {
364
+ const l = s(o, t), d = await p(l, "utf-8"), h = b(d), u = t.replace(/\.(yaml|yml)$/, "");
365
+ n.locales[u] = h ?? {};
462
366
  }
463
367
  } catch {
464
368
  }
465
369
  try {
466
- const r = s(e, "dialogues"), c = await y(r);
467
- for (const t of c)
468
- if (p(t) === ".dlg") {
469
- const l = s(r, t), d = await f(l, "utf-8"), m = t.replace(".dlg", ""), u = E(d, m);
470
- o.dialogues[u.id] = u, a.set(u.id, w(process.cwd(), l));
370
+ const o = s(e, "dialogues"), r = await y(o);
371
+ for (const t of r)
372
+ if (f(t) === ".dlg") {
373
+ const l = s(o, t), d = await p(l, "utf-8"), h = t.replace(".dlg", ""), u = v(d, h);
374
+ n.dialogues[u.id] = u, i.set(u.id, w(process.cwd(), l));
471
375
  }
472
376
  } catch {
473
377
  }
474
- return { registry: o, fileMap: a };
378
+ return { registry: n, fileMap: i };
475
379
  }
476
- async function Y() {
477
- const e = process.cwd(), o = s(e, "content");
478
- console.log(""), console.log(i.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(i.dim("Validating content..."));
380
+ async function z() {
381
+ const e = process.cwd(), n = s(e, "content");
382
+ console.log(""), console.log(c.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(c.dim("Validating content..."));
383
+ let i;
479
384
  try {
480
- const { registry: a, fileMap: n } = await P(o), r = N(a, n);
481
- D(r), r.length > 0 && (console.log(i.red("Build failed due to validation errors.")), console.log(""), process.exit(1));
385
+ const { registry: a, fileMap: o, config: r } = await X(n), t = I(a, o);
386
+ T(t), t.length > 0 && (console.log(c.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), i = { registry: a, config: r };
482
387
  } catch (a) {
483
- console.error(i.red("Error loading content:"), a), process.exit(1);
388
+ console.error(c.red("Error loading content:"), a), process.exit(1);
484
389
  }
485
390
  console.log("");
486
391
  try {
487
- await G({
392
+ await $({
488
393
  root: e,
489
- plugins: [$()],
394
+ plugins: [A()],
490
395
  build: {
491
396
  outDir: "dist",
492
397
  emptyOutDir: !0
493
398
  }
494
- }), console.log(""), console.log(i.green("✅ Build complete! Output in dist/")), console.log(""), console.log("To preview the build:"), console.log(i.dim(" yarn preview")), console.log("");
399
+ });
400
+ const a = s(e, "dist", "api");
401
+ await x(a, { recursive: !0 }), await m(s(a, "content"), JSON.stringify(i)), console.log(""), console.log(c.green("✅ Build complete! Output in dist/")), console.log(""), console.log("To preview the build:"), console.log(c.dim(" yarn preview")), console.log("");
495
402
  } catch (a) {
496
- console.error(i.red("Build failed:"), a), process.exit(1);
403
+ console.error(c.red("Build failed:"), a), process.exit(1);
497
404
  }
498
405
  }
499
- async function P(e) {
500
- const o = {
406
+ async function X(e) {
407
+ const n = {
501
408
  locations: {},
502
409
  characters: {},
503
410
  items: {},
@@ -505,59 +412,68 @@ async function P(e) {
505
412
  dialogues: {},
506
413
  quests: {},
507
414
  journalEntries: {},
415
+ interludes: {},
508
416
  locales: {}
509
- }, a = /* @__PURE__ */ new Map(), n = [
417
+ }, i = /* @__PURE__ */ new Map(), a = [
510
418
  { dir: "locations", key: "locations" },
511
419
  { dir: "characters", key: "characters" },
512
420
  { dir: "items", key: "items" },
513
421
  { dir: "maps", key: "maps" },
514
422
  { dir: "quests", key: "quests" },
515
- { dir: "journal", key: "journalEntries" }
423
+ { dir: "journal", key: "journalEntries" },
424
+ { dir: "interludes", key: "interludes" }
516
425
  ];
517
- for (const { dir: r, key: c } of n) {
518
- const t = s(e, r);
426
+ for (const { dir: r, key: t } of a) {
427
+ const l = s(e, r);
519
428
  try {
520
- const l = await y(t);
521
- for (const d of l)
522
- if (p(d) === ".yaml" || p(d) === ".yml") {
523
- const m = s(t, d), u = await f(m, "utf-8"), g = b(u);
524
- g && g.id && (o[c][g.id] = g, a.set(g.id, w(process.cwd(), m)));
429
+ const d = await y(l);
430
+ for (const h of d)
431
+ if (f(h) === ".yaml" || f(h) === ".yml") {
432
+ const u = s(l, h), g = await p(u, "utf-8"), E = b(g);
433
+ E && E.id && (n[t][E.id] = E, i.set(E.id, w(process.cwd(), u)));
525
434
  }
526
435
  } catch {
527
436
  }
528
437
  }
529
438
  try {
530
- const r = s(e, "locales"), c = await y(r);
531
- for (const t of c)
532
- if (p(t) === ".yaml" || p(t) === ".yml") {
533
- const l = s(r, t), d = await f(l, "utf-8"), m = b(d), u = t.replace(/\.(yaml|yml)$/, "");
534
- o.locales[u] = m ?? {};
439
+ const r = s(e, "locales"), t = await y(r);
440
+ for (const l of t)
441
+ if (f(l) === ".yaml" || f(l) === ".yml") {
442
+ const d = s(r, l), h = await p(d, "utf-8"), u = b(h), g = l.replace(/\.(yaml|yml)$/, "");
443
+ n.locales[g] = u ?? {};
535
444
  }
536
445
  } catch {
537
446
  }
538
447
  try {
539
- const r = s(e, "dialogues"), c = await y(r);
540
- for (const t of c)
541
- if (p(t) === ".dlg") {
542
- const l = s(r, t), d = await f(l, "utf-8"), m = t.replace(".dlg", ""), u = E(d, m);
543
- o.dialogues[u.id] = u, a.set(u.id, w(process.cwd(), l));
448
+ const r = s(e, "dialogues"), t = await y(r);
449
+ for (const l of t)
450
+ if (f(l) === ".dlg") {
451
+ const d = s(r, l), h = await p(d, "utf-8"), u = l.replace(".dlg", ""), g = v(h, u);
452
+ n.dialogues[g.id] = g, i.set(g.id, w(process.cwd(), d));
544
453
  }
545
454
  } catch {
546
455
  }
547
- return { registry: o, fileMap: a };
456
+ let o = null;
457
+ try {
458
+ const r = s(e, "game.yaml"), t = await p(r, "utf-8");
459
+ o = b(t);
460
+ } catch {
461
+ o = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
462
+ }
463
+ return { registry: n, fileMap: i, config: o };
548
464
  }
549
- async function K() {
550
- const e = process.cwd(), o = s(e, "content");
551
- console.log(""), console.log(i.bold.magenta("🐾 Validating Doodle Engine content...")), console.log("");
465
+ async function Z() {
466
+ const e = process.cwd(), n = s(e, "content");
467
+ console.log(""), console.log(c.bold.magenta("🐾 Validating Doodle Engine content...")), console.log("");
552
468
  try {
553
- const { registry: a, fileMap: n } = await z(o), r = N(a, n);
554
- D(r), r.length > 0 && process.exit(1);
555
- } catch (a) {
556
- console.error(i.red("Error loading content:"), a), process.exit(1);
469
+ const { registry: i, fileMap: a } = await ee(n), o = I(i, a);
470
+ T(o), o.length > 0 && process.exit(1);
471
+ } catch (i) {
472
+ console.error(c.red("Error loading content:"), i), process.exit(1);
557
473
  }
558
474
  }
559
- async function z(e) {
560
- const o = {
475
+ async function ee(e) {
476
+ const n = {
561
477
  locations: {},
562
478
  characters: {},
563
479
  items: {},
@@ -566,7 +482,7 @@ async function z(e) {
566
482
  quests: {},
567
483
  journalEntries: {},
568
484
  locales: {}
569
- }, a = /* @__PURE__ */ new Map(), n = [
485
+ }, i = /* @__PURE__ */ new Map(), a = [
570
486
  { dir: "locations", key: "locations" },
571
487
  { dir: "characters", key: "characters" },
572
488
  { dir: "items", key: "items" },
@@ -574,59 +490,60 @@ async function z(e) {
574
490
  { dir: "quests", key: "quests" },
575
491
  { dir: "journal", key: "journalEntries" }
576
492
  ];
577
- for (const { dir: r, key: c } of n) {
578
- const t = s(e, r);
493
+ for (const { dir: o, key: r } of a) {
494
+ const t = s(e, o);
579
495
  try {
580
496
  const l = await y(t);
581
497
  for (const d of l)
582
- if (p(d) === ".yaml" || p(d) === ".yml") {
583
- const m = s(t, d), u = await f(m, "utf-8"), g = b(u);
584
- g && g.id && (o[c][g.id] = g, a.set(g.id, w(process.cwd(), m)));
498
+ if (f(d) === ".yaml" || f(d) === ".yml") {
499
+ const h = s(t, d), u = await p(h, "utf-8"), g = b(u);
500
+ g && g.id && (n[r][g.id] = g, i.set(g.id, w(process.cwd(), h)));
585
501
  }
586
502
  } catch {
587
503
  }
588
504
  }
589
505
  try {
590
- const r = s(e, "locales"), c = await y(r);
591
- for (const t of c)
592
- if (p(t) === ".yaml" || p(t) === ".yml") {
593
- const l = s(r, t), d = await f(l, "utf-8"), m = b(d), u = t.replace(/\.(yaml|yml)$/, "");
594
- o.locales[u] = m ?? {};
506
+ const o = s(e, "locales"), r = await y(o);
507
+ for (const t of r)
508
+ if (f(t) === ".yaml" || f(t) === ".yml") {
509
+ const l = s(o, t), d = await p(l, "utf-8"), h = b(d), u = t.replace(/\.(yaml|yml)$/, "");
510
+ n.locales[u] = h ?? {};
595
511
  }
596
512
  } catch {
597
513
  }
598
514
  try {
599
- const r = s(e, "dialogues"), c = await y(r);
600
- for (const t of c)
601
- if (p(t) === ".dlg") {
602
- const l = s(r, t), d = await f(l, "utf-8"), m = t.replace(".dlg", ""), u = E(d, m);
603
- o.dialogues[u.id] = u, a.set(u.id, w(process.cwd(), l));
515
+ const o = s(e, "dialogues"), r = await y(o);
516
+ for (const t of r)
517
+ if (f(t) === ".dlg") {
518
+ const l = s(o, t), d = await p(l, "utf-8"), h = t.replace(".dlg", ""), u = v(d, h);
519
+ n.dialogues[u.id] = u, i.set(u.id, w(process.cwd(), l));
604
520
  }
605
521
  } catch {
606
522
  }
607
- return { registry: o, fileMap: a };
523
+ return { registry: n, fileMap: i };
608
524
  }
609
- const O = "🐾", X = "🐕", S = "🦴", Z = "✨", A = "📁", _ = "✅", ee = "🚀";
610
- async function te(e) {
611
- const o = s(process.cwd(), e);
612
- console.log(""), console.log(i.bold.magenta(` ${O} Doodle Engine ${O}`)), console.log(i.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${X} Creating new game: ${i.bold.cyan(e)}`), console.log("");
613
- const { useDefaultRenderer: a } = await H({
525
+ const D = "🐾", te = "🐕", R = "🦴", oe = "✨", q = "📁", _ = "✅", ae = "🚀";
526
+ async function ne(e) {
527
+ const n = s(process.cwd(), e);
528
+ console.log(""), console.log(c.bold.magenta(` ${D} Doodle Engine ${D}`)), console.log(c.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${te} Creating new game: ${c.bold.cyan(e)}`), console.log("");
529
+ const { useDefaultRenderer: i } = await M({
614
530
  type: "confirm",
615
531
  name: "useDefaultRenderer",
616
532
  message: "Use default renderer?",
617
533
  initial: !0
618
534
  });
619
- a === void 0 && (console.log(i.yellow(`
620
- ${S} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await oe(o, e, a), console.log(""), console.log(i.bold.green(` ${_} Project created successfully!`)), console.log(""), console.log(i.dim(` ${A} ${o}`)), console.log(""), console.log(i.bold(" Next steps:")), console.log(i.cyan(` cd ${e}`)), console.log(i.cyan(" npm install ") + i.dim("# or: yarn install / pnpm install")), console.log(i.cyan(" npm run dev ") + i.dim("# or: yarn dev / pnpm dev")), console.log(""), console.log(i.dim(` ${ee} Happy game making! ${O}`)), console.log("");
535
+ i === void 0 && (console.log(c.yellow(`
536
+ ${R} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await re(n, e, i), console.log(""), console.log(c.bold.green(` ${_} Project created successfully!`)), console.log(""), console.log(c.dim(` ${q} ${n}`)), console.log(""), console.log(c.bold(" Next steps:")), console.log(c.cyan(` cd ${e}`)), console.log(c.cyan(" npm install ") + c.dim("# or: yarn install / pnpm install")), console.log(c.cyan(" npm run dev ") + c.dim("# or: yarn dev / pnpm dev")), console.log(""), console.log(c.dim(` ${ae} Happy game making! ${D}`)), console.log("");
621
537
  }
622
- async function oe(e, o, a) {
623
- const n = [
538
+ async function re(e, n, i) {
539
+ const a = [
624
540
  "content/locations",
625
541
  "content/characters",
626
542
  "content/items",
627
543
  "content/dialogues",
628
544
  "content/quests",
629
545
  "content/journal",
546
+ "content/interludes",
630
547
  "content/locales",
631
548
  "content/maps",
632
549
  "assets/images/banners",
@@ -638,12 +555,12 @@ async function oe(e, o, a) {
638
555
  "assets/audio/voice",
639
556
  "src"
640
557
  ];
641
- console.log(` ${A} ${i.bold("Creating directories...")}`);
642
- for (const u of n)
643
- await j(s(e, u), { recursive: !0 });
644
- console.log(i.green(` ${_} Directories created`)), console.log("");
645
- const r = {
646
- name: o,
558
+ console.log(` ${q} ${c.bold("Creating directories...")}`);
559
+ for (const u of a)
560
+ await x(s(e, u), { recursive: !0 });
561
+ console.log(c.green(` ${_} Directories created`)), console.log("");
562
+ const o = {
563
+ name: n,
647
564
  version: "0.1.0",
648
565
  type: "module",
649
566
  scripts: {
@@ -666,11 +583,11 @@ async function oe(e, o, a) {
666
583
  vite: "^6.0.0"
667
584
  }
668
585
  };
669
- console.log(` ${Z} ${i.bold("Writing project files...")}`), await h(
586
+ console.log(` ${oe} ${c.bold("Writing project files...")}`), await m(
670
587
  s(e, "package.json"),
671
- JSON.stringify(r, null, 2)
588
+ JSON.stringify(o, null, 2)
672
589
  );
673
- const c = {
590
+ const r = {
674
591
  compilerOptions: {
675
592
  target: "ES2024",
676
593
  lib: ["ES2024", "DOM", "DOM.Iterable"],
@@ -687,10 +604,10 @@ async function oe(e, o, a) {
687
604
  },
688
605
  include: ["src"]
689
606
  };
690
- await h(
607
+ await m(
691
608
  s(e, "tsconfig.json"),
692
- JSON.stringify(c, null, 2)
693
- ), await h(s(e, "index.html"), `<!doctype html>
609
+ JSON.stringify(r, null, 2)
610
+ ), await m(s(e, "index.html"), `<!doctype html>
694
611
  <html lang="en">
695
612
  <head>
696
613
  <meta charset="UTF-8" />
@@ -702,7 +619,7 @@ async function oe(e, o, a) {
702
619
  <script type="module" src="/src/main.tsx"><\/script>
703
620
  </body>
704
621
  </html>
705
- `), await h(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
622
+ `), await m(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
706
623
  import { createRoot } from 'react-dom/client'
707
624
  import { App } from './App'
708
625
  import './index.css'
@@ -714,9 +631,9 @@ createRoot(document.getElementById('root')!).render(
714
631
  )
715
632
  `);
716
633
  let d;
717
- a ? d = `import { useEffect, useState } from 'react'
634
+ i ? d = `import { useEffect, useState } from 'react'
718
635
  import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
719
- import { GameShell } from '@doodle-engine/react'
636
+ import { GameShell, LoadingScreen } from '@doodle-engine/react'
720
637
 
721
638
  export function App() {
722
639
  const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
@@ -728,7 +645,7 @@ export function App() {
728
645
  }, [])
729
646
 
730
647
  if (!content) {
731
- return <div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>Loading game...</div>
648
+ return <LoadingScreen />
732
649
  }
733
650
 
734
651
  return (
@@ -739,13 +656,14 @@ export function App() {
739
656
  subtitle="A text-based adventure"
740
657
  splashDuration={2000}
741
658
  availableLocales={[{ code: 'en', label: 'English' }]}
659
+ devTools={import.meta.env.DEV}
742
660
  />
743
661
  )
744
662
  }
745
663
  ` : d = `import { useEffect, useState } from 'react'
746
664
  import { Engine } from '@doodle-engine/core'
747
665
  import type { GameState, Snapshot } from '@doodle-engine/core'
748
- import { GameProvider, useGame } from '@doodle-engine/react'
666
+ import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
749
667
 
750
668
  export function App() {
751
669
  const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
@@ -761,11 +679,11 @@ export function App() {
761
679
  }, [])
762
680
 
763
681
  if (!game) {
764
- return <div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>Loading game...</div>
682
+ return <LoadingScreen />
765
683
  }
766
684
 
767
685
  return (
768
- <GameProvider engine={game.engine} initialSnapshot={game.snapshot}>
686
+ <GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
769
687
  <GameUI />
770
688
  </GameProvider>
771
689
  )
@@ -833,7 +751,7 @@ function createEmptyState(): GameState {
833
751
  currentLocale: 'en',
834
752
  }
835
753
  }
836
- `, await h(s(e, "src/App.tsx"), d), await h(s(e, "src/index.css"), `body {
754
+ `, await m(s(e, "src/App.tsx"), d), await m(s(e, "src/index.css"), `body {
837
755
  margin: 0;
838
756
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
839
757
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -841,7 +759,7 @@ function createEmptyState(): GameState {
841
759
  -webkit-font-smoothing: antialiased;
842
760
  -moz-osx-font-smoothing: grayscale;
843
761
  }
844
- `), console.log(i.green(` ${_} Source files created`)), console.log(""), console.log(` ${S} ${i.bold("Writing starter content...")}`), await h(s(e, "content/game.yaml"), `# Game Configuration
762
+ `), console.log(c.green(` ${_} Source files created`)), console.log(""), console.log(` ${R} ${c.bold("Writing starter content...")}`), await m(s(e, "content/game.yaml"), `# Game Configuration
845
763
  startLocation: tavern
846
764
  startTime:
847
765
  day: 1
@@ -852,40 +770,40 @@ startVariables:
852
770
  reputation: 0
853
771
  _drinksBought: 0
854
772
  startInventory: []
855
- `), await h(s(e, "content/locations/tavern.yaml"), `id: tavern
773
+ `), await m(s(e, "content/locations/tavern.yaml"), `id: tavern
856
774
  name: "@location.tavern.name"
857
775
  description: "@location.tavern.description"
858
776
  banner: ""
859
777
  music: ""
860
778
  ambient: ""
861
- `), await h(s(e, "content/locations/market.yaml"), `id: market
779
+ `), await m(s(e, "content/locations/market.yaml"), `id: market
862
780
  name: "@location.market.name"
863
781
  description: "@location.market.description"
864
782
  banner: ""
865
783
  music: ""
866
784
  ambient: ""
867
- `), await h(s(e, "content/characters/bartender.yaml"), `id: bartender
785
+ `), await m(s(e, "content/characters/bartender.yaml"), `id: bartender
868
786
  name: "@character.bartender.name"
869
787
  biography: "@character.bartender.bio"
870
788
  portrait: ""
871
789
  location: tavern
872
790
  dialogue: bartender_greeting
873
791
  stats: {}
874
- `), await h(s(e, "content/characters/merchant.yaml"), `id: merchant
792
+ `), await m(s(e, "content/characters/merchant.yaml"), `id: merchant
875
793
  name: "@character.merchant.name"
876
794
  biography: "@character.merchant.bio"
877
795
  portrait: ""
878
796
  location: market
879
797
  dialogue: merchant_intro
880
798
  stats: {}
881
- `), await h(s(e, "content/items/old_coin.yaml"), `id: old_coin
799
+ `), await m(s(e, "content/items/old_coin.yaml"), `id: old_coin
882
800
  name: "@item.old_coin.name"
883
801
  description: "@item.old_coin.description"
884
802
  icon: ""
885
803
  image: ""
886
804
  location: tavern
887
805
  stats: {}
888
- `), await h(s(e, "content/maps/town.yaml"), `id: town
806
+ `), await m(s(e, "content/maps/town.yaml"), `id: town
889
807
  name: "@map.town.name"
890
808
  image: ""
891
809
  scale: 1
@@ -896,7 +814,7 @@ locations:
896
814
  - id: market
897
815
  x: 300
898
816
  y: 150
899
- `), await h(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
817
+ `), await m(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
900
818
  name: "@quest.odd_jobs.name"
901
819
  description: "@quest.odd_jobs.description"
902
820
  stages:
@@ -906,40 +824,90 @@ stages:
906
824
  description: "@quest.odd_jobs.stage.talked_to_merchant"
907
825
  - id: complete
908
826
  description: "@quest.odd_jobs.stage.complete"
909
- `), await h(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
827
+ `), await m(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
910
828
  title: "@journal.tavern_discovery.title"
911
829
  text: "@journal.tavern_discovery.text"
912
830
  category: places
913
- `), await h(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
831
+ `), await m(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
914
832
  title: "@journal.odd_jobs_accepted.title"
915
833
  text: "@journal.odd_jobs_accepted.text"
916
834
  category: quests
917
- `), await h(s(e, "content/journal/market_square.yaml"), `id: market_square
835
+ `), await m(s(e, "content/journal/market_square.yaml"), `id: market_square
918
836
  title: "@journal.market_square.title"
919
837
  text: "@journal.market_square.text"
920
838
  category: places
921
- `), await h(s(e, "content/dialogues/tavern_intro.dlg"), `TRIGGER tavern
839
+ `), await m(s(e, "content/interludes/chapter_one.yaml"), `id: chapter_one
840
+ # Background image shown fullscreen during the interlude.
841
+ # Put your image in assets/images/ and reference it here.
842
+ background: /assets/images/banners/tavern_banner.jpg
843
+
844
+ # Optional: decorative border/frame image overlaid on the background.
845
+ # banner: /assets/images/ui/chapter_frame.png
846
+
847
+ # Optional: music to play during this interlude.
848
+ # music: /assets/audio/music/chapter_theme.ogg
849
+
850
+ # The narrative text. Use @localization.key for multi-language support,
851
+ # or write plain text directly.
852
+ text: |
853
+ Chapter One: A New Beginning
854
+
855
+ The road behind you stretches long and empty.
856
+ Ahead, the lights of town flicker through the evening mist.
857
+
858
+ You have heard the rumours. Strange things happening.
859
+ People going missing. Shadows that move wrong.
860
+
861
+ Someone has to look into it.
862
+
863
+ It might as well be you.
864
+
865
+ # triggerLocation: tavern
866
+ # triggerConditions:
867
+ # - type: notFlag
868
+ # flag: seenChapterOne
869
+ `), await m(s(e, "content/dialogues/tavern_intro.dlg"), `# This dialogue triggers automatically when the player enters the tavern.
870
+ # TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
871
+ # Use notFlag to make it a one-time intro.
872
+
873
+ TRIGGER tavern
922
874
  REQUIRE notFlag seenTavernIntro
923
875
 
876
+ # Each NODE is a conversation point. The first NODE is always the start.
924
877
  NODE start
878
+ # NARRATOR: has no speaker — used for scene-setting text.
879
+ # @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
880
+ # You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
925
881
  NARRATOR: @narrator.tavern_intro
882
+
883
+ # Effects run immediately when this node is reached, before choices are shown.
926
884
  SET flag seenTavernIntro
927
885
 
886
+ # CHOICE text can use a @key or "inline text".
887
+ # A choice with END dialogue is a terminal choice — no GOTO needed.
928
888
  CHOICE @narrator.choice.look_around
929
889
  END dialogue
930
890
  END
931
- `), await h(s(e, "content/dialogues/market_intro.dlg"), `TRIGGER market
891
+ `), await m(s(e, "content/dialogues/market_intro.dlg"), `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
892
+
893
+ TRIGGER market
932
894
  REQUIRE notFlag seenMarketIntro
933
895
 
934
896
  NODE start
935
897
  NARRATOR: @narrator.market_intro
936
898
  SET flag seenMarketIntro
899
+
900
+ # ADD journalEntry unlocks a journal entry for the player.
937
901
  ADD journalEntry market_square
938
902
 
939
903
  CHOICE @narrator.choice.look_around
940
904
  END dialogue
941
905
  END
942
- `), await h(s(e, "content/dialogues/bartender_greeting.dlg"), `NODE start
906
+ `), await m(s(e, "content/dialogues/bartender_greeting.dlg"), `# This dialogue is triggered by clicking the bartender character.
907
+ # SPEAKER: lines set who's talking — matched to character ID (case-insensitive).
908
+ # Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
909
+
910
+ NODE start
943
911
  BARTENDER: @bartender.greeting
944
912
 
945
913
  # Always available — ask for rumors (demonstrates: flag, relationship, journalEntry)
@@ -1077,7 +1045,10 @@ NODE work_done
1077
1045
  NODE farewell
1078
1046
  BARTENDER: @bartender.farewell
1079
1047
  END dialogue
1080
- `), await h(s(e, "content/dialogues/merchant_intro.dlg"), `NODE start
1048
+ `), await m(s(e, "content/dialogues/merchant_intro.dlg"), `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
1049
+ # The quest choices here demonstrate multi-stage quest gating with questAtStage.
1050
+
1051
+ NODE start
1081
1052
  MERCHANT: @merchant.greeting
1082
1053
 
1083
1054
  CHOICE @merchant.choice.browse_wares
@@ -1177,7 +1148,7 @@ NODE about_market
1177
1148
  NODE farewell
1178
1149
  MERCHANT: @merchant.farewell
1179
1150
  END dialogue
1180
- `), await h(s(e, "content/locales/en.yaml"), `# ===================
1151
+ `), await m(s(e, "content/locales/en.yaml"), `# ===================
1181
1152
  # Narrator Intros
1182
1153
  # ===================
1183
1154
  narrator.tavern_intro: "You push open the heavy oak door and step inside. The warmth hits you first, then the smell — stale ale, wood smoke, and something frying in the kitchen. A few patrons hunch over their mugs. Behind the bar, a broad-shouldered man wipes down glasses, watching you with quiet interest."
@@ -1305,24 +1276,24 @@ notification.quest_updated: "Quest Updated: Odd Jobs"
1305
1276
  notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
1306
1277
  notification.bought_drink: "Bought a drink (-5 gold)"
1307
1278
  notification.bought_map: "Bought a map (-20 gold)"
1308
- `), console.log(i.green(` ${_} Starter content created`)), console.log(""), console.log(i.dim(" Content includes:")), console.log(i.dim(" 2 locations (tavern, market)")), console.log(i.dim(" 2 characters (bartender, merchant)")), console.log(i.dim(" 1 item (old coin)")), console.log(i.dim(" 1 map (town)")), console.log(i.dim(" 1 quest with 3 stages")), console.log(i.dim(" 3 journal entries")), console.log(i.dim(" 4 dialogues (2 narrator intros, 2 NPC conversations)")), console.log(i.dim(" English locale with all strings")), await h(s(e, ".gitignore"), `node_modules
1279
+ `), console.log(c.green(` ${_} Starter content created`)), console.log(""), console.log(c.dim(" Content includes:")), console.log(c.dim(" 2 locations (tavern, market)")), console.log(c.dim(" 2 characters (bartender, merchant)")), console.log(c.dim(" 1 item (old coin)")), console.log(c.dim(" 1 map (town)")), console.log(c.dim(" 1 quest with 3 stages")), console.log(c.dim(" 3 journal entries")), console.log(c.dim(" 4 dialogues (2 narrator intros, 2 NPC conversations)")), console.log(c.dim(" English locale with all strings")), await m(s(e, ".gitignore"), `node_modules
1309
1280
  dist
1310
1281
  .DS_Store
1311
1282
  *.log
1312
1283
  `);
1313
1284
  }
1314
- const k = new I();
1315
- k.name("doodle").description(i.magenta("🐾 Doodle Engine") + i.dim(" — Narrative RPG development tools")).version("0.0.1");
1285
+ const k = new G();
1286
+ k.name("doodle").description(c.magenta("🐾 Doodle Engine") + c.dim(" — Narrative RPG development tools")).version("0.0.1");
1316
1287
  k.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
1317
- await te(e);
1288
+ await ne(e);
1318
1289
  });
1319
1290
  k.command("dev").description("Start development server with hot reload").action(async () => {
1320
- await W();
1291
+ await Y();
1321
1292
  });
1322
1293
  k.command("build").description("Build game for production").action(async () => {
1323
- await Y();
1294
+ await z();
1324
1295
  });
1325
1296
  k.command("validate").description("Validate game content").action(async () => {
1326
- await K();
1297
+ await Z();
1327
1298
  });
1328
1299
  k.parse();