@doodle-engine/cli 0.0.7 → 0.0.9

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,57 +1,255 @@
1
1
  #!/usr/bin/env node
2
- import { Command as C } from "commander";
3
- import { crayon as t } from "crayon.js";
4
- import { createServer as T, build as N } from "vite";
5
- import O from "@vitejs/plugin-react";
6
- import { watch as I } from "chokidar";
7
- import { readdir as _, readFile as y, mkdir as S, writeFile as a } from "fs/promises";
8
- import { join as o, extname as g } from "path";
9
- import { parse as w } from "yaml";
10
- import { parseDialogue as R } from "@doodle-engine/core";
11
- import A from "prompts";
12
- const E = "🐾", G = "✨", q = "✏️", H = "➕";
13
- async function j() {
14
- const e = process.cwd(), s = o(e, "content");
15
- console.log(""), console.log(t.bold.magenta(` ${E} Doodle Engine Dev Server ${E}`)), console.log("");
16
- const d = {
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 R, writeFile as g } from "fs/promises";
8
+ import { join as s, extname as f, relative as w } from "path";
9
+ import { parse as b } from "yaml";
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
+ }
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
+ });
26
+ }
27
+ return i.push(...J(e, n)), i;
28
+ }
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
+ suggestion: "Node IDs must be unique within a dialogue"
36
+ }), a.add(o.id);
37
+ a.has(e.startNode) || i.push({
38
+ file: n,
39
+ message: `Start node "${e.startNode}" not found`,
40
+ suggestion: `Add a NODE ${e.startNode} or fix the startNode reference`
41
+ });
42
+ for (const o of e.nodes)
43
+ i.push(...B(o, a, n));
44
+ return i;
45
+ }
46
+ function B(e, n, i) {
47
+ const a = [];
48
+ if (e.next && !n.has(e.next) && a.push({
49
+ file: i,
50
+ message: `Node "${e.id}" GOTO "${e.next}" points to non-existent node`,
51
+ suggestion: `Add NODE ${e.next} or fix the GOTO target`
52
+ }), e.conditionalNext)
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));
72
+ }
73
+ if (e.conditions)
74
+ for (const o of e.conditions)
75
+ a.push(...O(o, e.id, i));
76
+ if (e.effects)
77
+ for (const o of e.effects)
78
+ a.push(...C(o, e.id, i));
79
+ return a;
80
+ }
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
+ };
123
+ function O(e, n, i) {
124
+ const a = [];
125
+ if (!e.type)
126
+ return a.push({
127
+ file: i,
128
+ message: `Node "${n}" has condition with missing type`
129
+ }), a;
130
+ if (e.type === "timeIs")
131
+ return e.hour === void 0 && e.day === void 0 && a.push({
132
+ file: i,
133
+ message: `Node "${n}" condition "timeIs" must have at least one of "hour" or "day" argument`
134
+ }), a;
135
+ const o = L[e.type];
136
+ if (!o)
137
+ return a;
138
+ for (const r of o)
139
+ (e[r] === void 0 || e[r] === null || e[r] === "") && a.push({
140
+ file: i,
141
+ message: `Node "${n}" condition "${e.type}" missing required "${r}" argument`
142
+ });
143
+ return a;
144
+ }
145
+ function C(e, n, i) {
146
+ const a = [];
147
+ if (!e.type)
148
+ return a.push({
149
+ file: i,
150
+ message: `Node "${n}" has effect with missing type`
151
+ }), a;
152
+ const o = U[e.type];
153
+ if (!o)
154
+ return a;
155
+ for (const r of o)
156
+ (e[r] === void 0 || e[r] === null || e[r] === "") && a.push({
157
+ file: i,
158
+ message: `Node "${n}" effect "${e.type}" missing required "${r}" argument`
159
+ });
160
+ return a;
161
+ }
162
+ function J(e, n) {
163
+ const i = [], a = /* @__PURE__ */ new Set();
164
+ for (const t of Object.values(e.locales))
165
+ for (const l of Object.keys(t))
166
+ a.add(l);
167
+ const o = (t) => t.startsWith("@"), r = (t, l, d) => {
168
+ const u = t.slice(1);
169
+ if (!a.has(u)) {
170
+ const h = n.get(l) || `${d}:${l}`;
171
+ i.push({
172
+ file: h,
173
+ message: `Localization key "${t}" not found in any locale file`,
174
+ suggestion: `Add "${u}: ..." to your locale files`
175
+ });
176
+ }
177
+ };
178
+ for (const t of Object.values(e.locations))
179
+ o(t.name) && r(t.name, t.id, "location"), o(t.description) && r(t.description, t.id, "location");
180
+ for (const t of Object.values(e.characters))
181
+ o(t.name) && r(t.name, t.id, "character"), o(t.biography) && r(t.biography, t.id, "character");
182
+ for (const t of Object.values(e.items))
183
+ o(t.name) && r(t.name, t.id, "item"), o(t.description) && r(t.description, t.id, "item");
184
+ for (const t of Object.values(e.quests)) {
185
+ o(t.name) && r(t.name, t.id, "quest"), o(t.description) && r(t.description, t.id, "quest");
186
+ for (const l of t.stages)
187
+ o(l.description) && r(l.description, t.id, "quest");
188
+ }
189
+ for (const t of Object.values(e.journalEntries))
190
+ o(t.title) && r(t.title, t.id, "journal"), o(t.text) && r(t.text, t.id, "journal");
191
+ for (const t of Object.values(e.dialogues))
192
+ for (const l of t.nodes) {
193
+ o(l.text) && r(l.text, t.id, "dialogue");
194
+ for (const d of l.choices)
195
+ o(d.text) && r(d.text, t.id, "dialogue");
196
+ }
197
+ return i;
198
+ }
199
+ function T(e) {
200
+ if (e.length === 0) {
201
+ console.log(c.green("✓ No validation errors"));
202
+ return;
203
+ }
204
+ console.log(c.red(`
205
+ ✗ Found ${e.length} validation error${e.length === 1 ? "" : "s"}:
206
+ `));
207
+ for (const n of e)
208
+ 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();
209
+ }
210
+ const N = "🐾", W = "✨", Q = "✏️", V = "➕";
211
+ async function Y() {
212
+ const e = process.cwd(), n = s(e, "content");
213
+ console.log(""), console.log(c.bold.magenta(` ${N} Doodle Engine Dev Server ${N}`)), console.log("");
214
+ const i = {
17
215
  name: "doodle-content-loader",
18
- configureServer(r) {
19
- r.middlewares.use("/api/content", async (n, l) => {
216
+ configureServer(o) {
217
+ o.middlewares.use("/api/content", async (t, l) => {
20
218
  try {
21
- const i = await x(s);
22
- l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(i));
23
- } catch (i) {
24
- console.error(t.red(" Error loading content:"), i), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
219
+ const d = await P(n);
220
+ l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(d));
221
+ } catch (d) {
222
+ console.error(c.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
25
223
  }
26
224
  });
27
- const c = I(o(s, "**/*"), {
225
+ const r = H(n, {
28
226
  ignored: /(^|[\/\\])\../,
29
227
  persistent: !0
30
228
  });
31
- c.on("change", (n) => {
32
- console.log(t.yellow(` ${q} Content changed: ${n}`)), r.ws.send({
229
+ r.on("change", async (t) => {
230
+ console.log(c.yellow(` ${Q} Content changed: ${t}`)), await S(n), o.ws.send({
33
231
  type: "full-reload",
34
232
  path: "*"
35
233
  });
36
- }), c.on("add", (n) => {
37
- console.log(t.green(` ${H} Content added: ${n}`)), r.ws.send({
234
+ }), r.on("add", async (t) => {
235
+ console.log(c.green(` ${V} Content added: ${t}`)), await S(n), o.ws.send({
38
236
  type: "full-reload",
39
237
  path: "*"
40
238
  });
41
239
  });
42
240
  }
43
- }, h = await T({
241
+ }, a = await j({
44
242
  root: e,
45
- plugins: [O(), d],
243
+ plugins: [A(), i],
46
244
  server: {
47
245
  port: 3e3,
48
246
  open: !0
49
247
  }
50
248
  });
51
- await h.listen(), h.printUrls(), console.log(""), console.log(t.dim(` ${G} Watching content files for changes...`)), console.log("");
249
+ await a.listen(), a.printUrls(), console.log(""), console.log(c.dim(` ${W} Watching content files for changes...`)), console.log("");
52
250
  }
53
- async function x(e) {
54
- const s = {
251
+ async function P(e) {
252
+ const n = {
55
253
  locations: {},
56
254
  characters: {},
57
255
  items: {},
@@ -61,8 +259,8 @@ async function x(e) {
61
259
  journalEntries: {},
62
260
  locales: {}
63
261
  };
64
- let d = null;
65
- const h = [
262
+ let i = null;
263
+ const a = [
66
264
  { dir: "locations", key: "locations" },
67
265
  { dir: "characters", key: "characters" },
68
266
  { dir: "items", key: "items" },
@@ -70,41 +268,41 @@ async function x(e) {
70
268
  { dir: "quests", key: "quests" },
71
269
  { dir: "journal", key: "journalEntries" }
72
270
  ];
73
- for (const { dir: r, key: c } of h) {
74
- const n = o(e, r);
271
+ for (const { dir: o, key: r } of a) {
272
+ const t = s(e, o);
75
273
  try {
76
- const l = await _(n);
77
- for (const i of l)
78
- if (g(i) === ".yaml" || g(i) === ".yml") {
79
- const u = o(n, i), m = await y(u, "utf-8"), b = w(m);
80
- b && b.id && (s[c][b.id] = b);
274
+ const l = await y(t);
275
+ for (const d of l)
276
+ if (f(d) === ".yaml" || f(d) === ".yml") {
277
+ const u = s(t, d), h = await p(u, "utf-8"), m = b(h);
278
+ m && m.id && (n[r][m.id] = m);
81
279
  }
82
280
  } catch {
83
281
  }
84
282
  }
85
283
  try {
86
- const r = o(e, "locales"), c = await _(r);
87
- for (const n of c)
88
- if (g(n) === ".yaml" || g(n) === ".yml") {
89
- const l = o(r, n), i = await y(l, "utf-8"), u = w(i), m = n.replace(/\.(yaml|yml)$/, "");
90
- s.locales[m] = u ?? {};
284
+ const o = s(e, "locales"), r = await y(o);
285
+ for (const t of r)
286
+ if (f(t) === ".yaml" || f(t) === ".yml") {
287
+ const l = s(o, t), d = await p(l, "utf-8"), u = b(d), h = t.replace(/\.(yaml|yml)$/, "");
288
+ n.locales[h] = u ?? {};
91
289
  }
92
290
  } catch {
93
291
  }
94
292
  try {
95
- const r = o(e, "dialogues"), c = await _(r);
96
- for (const n of c)
97
- if (g(n) === ".dlg") {
98
- const l = o(r, n), i = await y(l, "utf-8"), u = n.replace(".dlg", ""), m = R(i, u);
99
- s.dialogues[m.id] = m;
293
+ const o = s(e, "dialogues"), r = await y(o);
294
+ for (const t of r)
295
+ if (f(t) === ".dlg") {
296
+ const l = s(o, t), d = await p(l, "utf-8"), u = t.replace(".dlg", ""), h = v(d, u);
297
+ n.dialogues[h.id] = h;
100
298
  }
101
299
  } catch {
102
300
  }
103
301
  try {
104
- const r = o(e, "game.yaml"), c = await y(r, "utf-8");
105
- d = w(c);
302
+ const o = s(e, "game.yaml"), r = await p(o, "utf-8");
303
+ i = b(r);
106
304
  } catch {
107
- console.warn(t.yellow(" No game.yaml found, using defaults")), d = {
305
+ console.warn(c.yellow(" No game.yaml found, using defaults")), i = {
108
306
  startLocation: "tavern",
109
307
  startTime: { day: 1, hour: 8 },
110
308
  startFlags: {},
@@ -112,43 +310,224 @@ async function x(e) {
112
310
  startInventory: []
113
311
  };
114
312
  }
115
- return { registry: s, config: d };
313
+ return { registry: n, config: i };
116
314
  }
117
- async function M() {
118
- const e = process.cwd();
119
- console.log(`🐕🎮 Building Doodle Engine game...
120
- `);
315
+ async function S(e) {
121
316
  try {
122
- await N({
317
+ const { registry: n, fileMap: i } = await K(e), a = I(n, i);
318
+ a.length > 0 && (console.log(""), T(a), console.log(""));
319
+ } catch (n) {
320
+ console.error(c.red(" Error running validation:"), n);
321
+ }
322
+ }
323
+ async function K(e) {
324
+ const n = {
325
+ locations: {},
326
+ characters: {},
327
+ items: {},
328
+ maps: {},
329
+ dialogues: {},
330
+ quests: {},
331
+ journalEntries: {},
332
+ locales: {}
333
+ }, i = /* @__PURE__ */ new Map(), a = [
334
+ { dir: "locations", key: "locations" },
335
+ { dir: "characters", key: "characters" },
336
+ { dir: "items", key: "items" },
337
+ { dir: "maps", key: "maps" },
338
+ { dir: "quests", key: "quests" },
339
+ { dir: "journal", key: "journalEntries" }
340
+ ];
341
+ for (const { dir: o, key: r } of a) {
342
+ const t = s(e, o);
343
+ try {
344
+ const l = await y(t);
345
+ for (const d of l)
346
+ if (f(d) === ".yaml" || f(d) === ".yml") {
347
+ const u = s(t, d), h = await p(u, "utf-8"), m = b(h);
348
+ m && m.id && (n[r][m.id] = m, i.set(m.id, w(process.cwd(), u)));
349
+ }
350
+ } catch {
351
+ }
352
+ }
353
+ try {
354
+ const o = s(e, "locales"), r = await y(o);
355
+ for (const t of r)
356
+ if (f(t) === ".yaml" || f(t) === ".yml") {
357
+ const l = s(o, t), d = await p(l, "utf-8"), u = b(d), h = t.replace(/\.(yaml|yml)$/, "");
358
+ n.locales[h] = u ?? {};
359
+ }
360
+ } catch {
361
+ }
362
+ try {
363
+ const o = s(e, "dialogues"), r = await y(o);
364
+ for (const t of r)
365
+ if (f(t) === ".dlg") {
366
+ const l = s(o, t), d = await p(l, "utf-8"), u = t.replace(".dlg", ""), h = v(d, u);
367
+ n.dialogues[h.id] = h, i.set(h.id, w(process.cwd(), l));
368
+ }
369
+ } catch {
370
+ }
371
+ return { registry: n, fileMap: i };
372
+ }
373
+ async function z() {
374
+ const e = process.cwd(), n = s(e, "content");
375
+ console.log(""), console.log(c.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(c.dim("Validating content..."));
376
+ let i;
377
+ try {
378
+ const { registry: a, fileMap: o, config: r } = await X(n), t = I(a, o);
379
+ 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 };
380
+ } catch (a) {
381
+ console.error(c.red("Error loading content:"), a), process.exit(1);
382
+ }
383
+ console.log("");
384
+ try {
385
+ await $({
123
386
  root: e,
124
- plugins: [O()],
387
+ plugins: [A()],
125
388
  build: {
126
389
  outDir: "dist",
127
390
  emptyOutDir: !0
128
391
  }
129
- }), console.log(`
130
- Build complete! Output in dist/
131
- `), console.log("To preview the build:"), console.log(` yarn preview
132
- `);
133
- } catch (s) {
134
- console.error("Build failed:", s), process.exit(1);
392
+ });
393
+ const a = s(e, "dist", "api");
394
+ await R(a, { recursive: !0 }), await g(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("");
395
+ } catch (a) {
396
+ console.error(c.red("Build failed:"), a), process.exit(1);
135
397
  }
136
398
  }
137
- const k = "🐾", $ = "🐕", v = "🦴", B = "✨", D = "📁", f = "✅", F = "🚀";
138
- async function U(e) {
139
- const s = o(process.cwd(), e);
140
- console.log(""), console.log(t.bold.magenta(` ${k} Doodle Engine ${k}`)), console.log(t.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${$} Creating new game: ${t.bold.cyan(e)}`), console.log("");
141
- const { useDefaultRenderer: d } = await A({
399
+ async function X(e) {
400
+ const n = {
401
+ locations: {},
402
+ characters: {},
403
+ items: {},
404
+ maps: {},
405
+ dialogues: {},
406
+ quests: {},
407
+ journalEntries: {},
408
+ locales: {}
409
+ }, i = /* @__PURE__ */ new Map(), a = [
410
+ { dir: "locations", key: "locations" },
411
+ { dir: "characters", key: "characters" },
412
+ { dir: "items", key: "items" },
413
+ { dir: "maps", key: "maps" },
414
+ { dir: "quests", key: "quests" },
415
+ { dir: "journal", key: "journalEntries" }
416
+ ];
417
+ for (const { dir: r, key: t } of a) {
418
+ const l = s(e, r);
419
+ try {
420
+ const d = await y(l);
421
+ for (const u of d)
422
+ if (f(u) === ".yaml" || f(u) === ".yml") {
423
+ const h = s(l, u), m = await p(h, "utf-8"), k = b(m);
424
+ k && k.id && (n[t][k.id] = k, i.set(k.id, w(process.cwd(), h)));
425
+ }
426
+ } catch {
427
+ }
428
+ }
429
+ try {
430
+ const r = s(e, "locales"), t = await y(r);
431
+ for (const l of t)
432
+ if (f(l) === ".yaml" || f(l) === ".yml") {
433
+ const d = s(r, l), u = await p(d, "utf-8"), h = b(u), m = l.replace(/\.(yaml|yml)$/, "");
434
+ n.locales[m] = h ?? {};
435
+ }
436
+ } catch {
437
+ }
438
+ try {
439
+ const r = s(e, "dialogues"), t = await y(r);
440
+ for (const l of t)
441
+ if (f(l) === ".dlg") {
442
+ const d = s(r, l), u = await p(d, "utf-8"), h = l.replace(".dlg", ""), m = v(u, h);
443
+ n.dialogues[m.id] = m, i.set(m.id, w(process.cwd(), d));
444
+ }
445
+ } catch {
446
+ }
447
+ let o = null;
448
+ try {
449
+ const r = s(e, "game.yaml"), t = await p(r, "utf-8");
450
+ o = b(t);
451
+ } catch {
452
+ o = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
453
+ }
454
+ return { registry: n, fileMap: i, config: o };
455
+ }
456
+ async function Z() {
457
+ const e = process.cwd(), n = s(e, "content");
458
+ console.log(""), console.log(c.bold.magenta("🐾 Validating Doodle Engine content...")), console.log("");
459
+ try {
460
+ const { registry: i, fileMap: a } = await ee(n), o = I(i, a);
461
+ T(o), o.length > 0 && process.exit(1);
462
+ } catch (i) {
463
+ console.error(c.red("Error loading content:"), i), process.exit(1);
464
+ }
465
+ }
466
+ async function ee(e) {
467
+ const n = {
468
+ locations: {},
469
+ characters: {},
470
+ items: {},
471
+ maps: {},
472
+ dialogues: {},
473
+ quests: {},
474
+ journalEntries: {},
475
+ locales: {}
476
+ }, i = /* @__PURE__ */ new Map(), a = [
477
+ { dir: "locations", key: "locations" },
478
+ { dir: "characters", key: "characters" },
479
+ { dir: "items", key: "items" },
480
+ { dir: "maps", key: "maps" },
481
+ { dir: "quests", key: "quests" },
482
+ { dir: "journal", key: "journalEntries" }
483
+ ];
484
+ for (const { dir: o, key: r } of a) {
485
+ const t = s(e, o);
486
+ try {
487
+ const l = await y(t);
488
+ for (const d of l)
489
+ if (f(d) === ".yaml" || f(d) === ".yml") {
490
+ const u = s(t, d), h = await p(u, "utf-8"), m = b(h);
491
+ m && m.id && (n[r][m.id] = m, i.set(m.id, w(process.cwd(), u)));
492
+ }
493
+ } catch {
494
+ }
495
+ }
496
+ try {
497
+ const o = s(e, "locales"), r = await y(o);
498
+ for (const t of r)
499
+ if (f(t) === ".yaml" || f(t) === ".yml") {
500
+ const l = s(o, t), d = await p(l, "utf-8"), u = b(d), h = t.replace(/\.(yaml|yml)$/, "");
501
+ n.locales[h] = u ?? {};
502
+ }
503
+ } catch {
504
+ }
505
+ try {
506
+ const o = s(e, "dialogues"), r = await y(o);
507
+ for (const t of r)
508
+ if (f(t) === ".dlg") {
509
+ const l = s(o, t), d = await p(l, "utf-8"), u = t.replace(".dlg", ""), h = v(d, u);
510
+ n.dialogues[h.id] = h, i.set(h.id, w(process.cwd(), l));
511
+ }
512
+ } catch {
513
+ }
514
+ return { registry: n, fileMap: i };
515
+ }
516
+ const D = "🐾", te = "🐕", q = "🦴", oe = "✨", x = "📁", _ = "✅", ae = "🚀";
517
+ async function ne(e) {
518
+ const n = s(process.cwd(), e);
519
+ 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("");
520
+ const { useDefaultRenderer: i } = await M({
142
521
  type: "confirm",
143
522
  name: "useDefaultRenderer",
144
523
  message: "Use default renderer?",
145
524
  initial: !0
146
525
  });
147
- d === void 0 && (console.log(t.yellow(`
148
- ${v} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await J(s, e, d), console.log(""), console.log(t.bold.green(` ${f} Project created successfully!`)), console.log(""), console.log(t.dim(` ${D} ${s}`)), console.log(""), console.log(t.bold(" Next steps:")), console.log(t.cyan(` cd ${e}`)), console.log(t.cyan(" npm install ") + t.dim("# or: yarn install / pnpm install")), console.log(t.cyan(" npm run dev ") + t.dim("# or: yarn dev / pnpm dev")), console.log(""), console.log(t.dim(` ${F} Happy game making! ${k}`)), console.log("");
526
+ i === void 0 && (console.log(c.yellow(`
527
+ ${q} 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(` ${x} ${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("");
149
528
  }
150
- async function J(e, s, d) {
151
- const h = [
529
+ async function re(e, n, i) {
530
+ const a = [
152
531
  "content/locations",
153
532
  "content/characters",
154
533
  "content/items",
@@ -166,12 +545,12 @@ async function J(e, s, d) {
166
545
  "assets/audio/voice",
167
546
  "src"
168
547
  ];
169
- console.log(` ${D} ${t.bold("Creating directories...")}`);
170
- for (const m of h)
171
- await S(o(e, m), { recursive: !0 });
172
- console.log(t.green(` ${f} Directories created`)), console.log("");
173
- const r = {
174
- name: s,
548
+ console.log(` ${x} ${c.bold("Creating directories...")}`);
549
+ for (const h of a)
550
+ await R(s(e, h), { recursive: !0 });
551
+ console.log(c.green(` ${_} Directories created`)), console.log("");
552
+ const o = {
553
+ name: n,
175
554
  version: "0.1.0",
176
555
  type: "module",
177
556
  scripts: {
@@ -194,11 +573,11 @@ async function J(e, s, d) {
194
573
  vite: "^6.0.0"
195
574
  }
196
575
  };
197
- console.log(` ${B} ${t.bold("Writing project files...")}`), await a(
198
- o(e, "package.json"),
199
- JSON.stringify(r, null, 2)
576
+ console.log(` ${oe} ${c.bold("Writing project files...")}`), await g(
577
+ s(e, "package.json"),
578
+ JSON.stringify(o, null, 2)
200
579
  );
201
- const c = {
580
+ const r = {
202
581
  compilerOptions: {
203
582
  target: "ES2024",
204
583
  lib: ["ES2024", "DOM", "DOM.Iterable"],
@@ -215,10 +594,10 @@ async function J(e, s, d) {
215
594
  },
216
595
  include: ["src"]
217
596
  };
218
- await a(
219
- o(e, "tsconfig.json"),
220
- JSON.stringify(c, null, 2)
221
- ), await a(o(e, "index.html"), `<!doctype html>
597
+ await g(
598
+ s(e, "tsconfig.json"),
599
+ JSON.stringify(r, null, 2)
600
+ ), await g(s(e, "index.html"), `<!doctype html>
222
601
  <html lang="en">
223
602
  <head>
224
603
  <meta charset="UTF-8" />
@@ -230,7 +609,7 @@ async function J(e, s, d) {
230
609
  <script type="module" src="/src/main.tsx"><\/script>
231
610
  </body>
232
611
  </html>
233
- `), await a(o(e, "src/main.tsx"), `import { StrictMode } from 'react'
612
+ `), await g(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
234
613
  import { createRoot } from 'react-dom/client'
235
614
  import { App } from './App'
236
615
  import './index.css'
@@ -241,10 +620,10 @@ createRoot(document.getElementById('root')!).render(
241
620
  </StrictMode>,
242
621
  )
243
622
  `);
244
- let i;
245
- d ? i = `import { useEffect, useState } from 'react'
623
+ let d;
624
+ i ? d = `import { useEffect, useState } from 'react'
246
625
  import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
247
- import { GameShell } from '@doodle-engine/react'
626
+ import { GameShell, LoadingScreen } from '@doodle-engine/react'
248
627
 
249
628
  export function App() {
250
629
  const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
@@ -256,7 +635,7 @@ export function App() {
256
635
  }, [])
257
636
 
258
637
  if (!content) {
259
- return <div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>Loading game...</div>
638
+ return <LoadingScreen />
260
639
  }
261
640
 
262
641
  return (
@@ -267,13 +646,14 @@ export function App() {
267
646
  subtitle="A text-based adventure"
268
647
  splashDuration={2000}
269
648
  availableLocales={[{ code: 'en', label: 'English' }]}
649
+ devTools={import.meta.env.DEV}
270
650
  />
271
651
  )
272
652
  }
273
- ` : i = `import { useEffect, useState } from 'react'
653
+ ` : d = `import { useEffect, useState } from 'react'
274
654
  import { Engine } from '@doodle-engine/core'
275
655
  import type { GameState, Snapshot } from '@doodle-engine/core'
276
- import { GameProvider, useGame } from '@doodle-engine/react'
656
+ import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
277
657
 
278
658
  export function App() {
279
659
  const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
@@ -289,11 +669,11 @@ export function App() {
289
669
  }, [])
290
670
 
291
671
  if (!game) {
292
- return <div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>Loading game...</div>
672
+ return <LoadingScreen />
293
673
  }
294
674
 
295
675
  return (
296
- <GameProvider engine={game.engine} initialSnapshot={game.snapshot}>
676
+ <GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
297
677
  <GameUI />
298
678
  </GameProvider>
299
679
  )
@@ -361,7 +741,7 @@ function createEmptyState(): GameState {
361
741
  currentLocale: 'en',
362
742
  }
363
743
  }
364
- `, await a(o(e, "src/App.tsx"), i), await a(o(e, "src/index.css"), `body {
744
+ `, await g(s(e, "src/App.tsx"), d), await g(s(e, "src/index.css"), `body {
365
745
  margin: 0;
366
746
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
367
747
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -369,7 +749,7 @@ function createEmptyState(): GameState {
369
749
  -webkit-font-smoothing: antialiased;
370
750
  -moz-osx-font-smoothing: grayscale;
371
751
  }
372
- `), console.log(t.green(` ${f} Source files created`)), console.log(""), console.log(` ${v} ${t.bold("Writing starter content...")}`), await a(o(e, "content/game.yaml"), `# Game Configuration
752
+ `), console.log(c.green(` ${_} Source files created`)), console.log(""), console.log(` ${q} ${c.bold("Writing starter content...")}`), await g(s(e, "content/game.yaml"), `# Game Configuration
373
753
  startLocation: tavern
374
754
  startTime:
375
755
  day: 1
@@ -380,40 +760,40 @@ startVariables:
380
760
  reputation: 0
381
761
  _drinksBought: 0
382
762
  startInventory: []
383
- `), await a(o(e, "content/locations/tavern.yaml"), `id: tavern
763
+ `), await g(s(e, "content/locations/tavern.yaml"), `id: tavern
384
764
  name: "@location.tavern.name"
385
765
  description: "@location.tavern.description"
386
766
  banner: ""
387
767
  music: ""
388
768
  ambient: ""
389
- `), await a(o(e, "content/locations/market.yaml"), `id: market
769
+ `), await g(s(e, "content/locations/market.yaml"), `id: market
390
770
  name: "@location.market.name"
391
771
  description: "@location.market.description"
392
772
  banner: ""
393
773
  music: ""
394
774
  ambient: ""
395
- `), await a(o(e, "content/characters/bartender.yaml"), `id: bartender
775
+ `), await g(s(e, "content/characters/bartender.yaml"), `id: bartender
396
776
  name: "@character.bartender.name"
397
777
  biography: "@character.bartender.bio"
398
778
  portrait: ""
399
779
  location: tavern
400
780
  dialogue: bartender_greeting
401
781
  stats: {}
402
- `), await a(o(e, "content/characters/merchant.yaml"), `id: merchant
782
+ `), await g(s(e, "content/characters/merchant.yaml"), `id: merchant
403
783
  name: "@character.merchant.name"
404
784
  biography: "@character.merchant.bio"
405
785
  portrait: ""
406
786
  location: market
407
787
  dialogue: merchant_intro
408
788
  stats: {}
409
- `), await a(o(e, "content/items/old_coin.yaml"), `id: old_coin
789
+ `), await g(s(e, "content/items/old_coin.yaml"), `id: old_coin
410
790
  name: "@item.old_coin.name"
411
791
  description: "@item.old_coin.description"
412
792
  icon: ""
413
793
  image: ""
414
794
  location: tavern
415
795
  stats: {}
416
- `), await a(o(e, "content/maps/town.yaml"), `id: town
796
+ `), await g(s(e, "content/maps/town.yaml"), `id: town
417
797
  name: "@map.town.name"
418
798
  image: ""
419
799
  scale: 1
@@ -424,7 +804,7 @@ locations:
424
804
  - id: market
425
805
  x: 300
426
806
  y: 150
427
- `), await a(o(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
807
+ `), await g(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
428
808
  name: "@quest.odd_jobs.name"
429
809
  description: "@quest.odd_jobs.description"
430
810
  stages:
@@ -434,40 +814,60 @@ stages:
434
814
  description: "@quest.odd_jobs.stage.talked_to_merchant"
435
815
  - id: complete
436
816
  description: "@quest.odd_jobs.stage.complete"
437
- `), await a(o(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
817
+ `), await g(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
438
818
  title: "@journal.tavern_discovery.title"
439
819
  text: "@journal.tavern_discovery.text"
440
820
  category: places
441
- `), await a(o(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
821
+ `), await g(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
442
822
  title: "@journal.odd_jobs_accepted.title"
443
823
  text: "@journal.odd_jobs_accepted.text"
444
824
  category: quests
445
- `), await a(o(e, "content/journal/market_square.yaml"), `id: market_square
825
+ `), await g(s(e, "content/journal/market_square.yaml"), `id: market_square
446
826
  title: "@journal.market_square.title"
447
827
  text: "@journal.market_square.text"
448
828
  category: places
449
- `), await a(o(e, "content/dialogues/tavern_intro.dlg"), `TRIGGER tavern
829
+ `), await g(s(e, "content/dialogues/tavern_intro.dlg"), `# This dialogue triggers automatically when the player enters the tavern.
830
+ # TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
831
+ # Use notFlag to make it a one-time intro.
832
+
833
+ TRIGGER tavern
450
834
  REQUIRE notFlag seenTavernIntro
451
835
 
836
+ # Each NODE is a conversation point. The first NODE is always the start.
452
837
  NODE start
838
+ # NARRATOR: has no speaker — used for scene-setting text.
839
+ # @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
840
+ # You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
453
841
  NARRATOR: @narrator.tavern_intro
842
+
843
+ # Effects run immediately when this node is reached, before choices are shown.
454
844
  SET flag seenTavernIntro
455
845
 
846
+ # CHOICE text can use a @key or "inline text".
847
+ # A choice with END dialogue is a terminal choice — no GOTO needed.
456
848
  CHOICE @narrator.choice.look_around
457
849
  END dialogue
458
850
  END
459
- `), await a(o(e, "content/dialogues/market_intro.dlg"), `TRIGGER market
851
+ `), await g(s(e, "content/dialogues/market_intro.dlg"), `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
852
+
853
+ TRIGGER market
460
854
  REQUIRE notFlag seenMarketIntro
461
855
 
462
856
  NODE start
463
857
  NARRATOR: @narrator.market_intro
464
858
  SET flag seenMarketIntro
859
+
860
+ # ADD journalEntry unlocks a journal entry for the player.
465
861
  ADD journalEntry market_square
466
862
 
467
863
  CHOICE @narrator.choice.look_around
468
864
  END dialogue
469
865
  END
470
- `), await a(o(e, "content/dialogues/bartender_greeting.dlg"), `NODE start
866
+ `), await g(s(e, "content/dialogues/bartender_greeting.dlg"), `# This dialogue is triggered by clicking the bartender character.
867
+ # SPEAKER: lines set who's talking — matched to character ID (case-insensitive).
868
+ # Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
869
+
870
+ NODE start
471
871
  BARTENDER: @bartender.greeting
472
872
 
473
873
  # Always available — ask for rumors (demonstrates: flag, relationship, journalEntry)
@@ -605,7 +1005,10 @@ NODE work_done
605
1005
  NODE farewell
606
1006
  BARTENDER: @bartender.farewell
607
1007
  END dialogue
608
- `), await a(o(e, "content/dialogues/merchant_intro.dlg"), `NODE start
1008
+ `), await g(s(e, "content/dialogues/merchant_intro.dlg"), `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
1009
+ # The quest choices here demonstrate multi-stage quest gating with questAtStage.
1010
+
1011
+ NODE start
609
1012
  MERCHANT: @merchant.greeting
610
1013
 
611
1014
  CHOICE @merchant.choice.browse_wares
@@ -705,7 +1108,7 @@ NODE about_market
705
1108
  NODE farewell
706
1109
  MERCHANT: @merchant.farewell
707
1110
  END dialogue
708
- `), await a(o(e, "content/locales/en.yaml"), `# ===================
1111
+ `), await g(s(e, "content/locales/en.yaml"), `# ===================
709
1112
  # Narrator Intros
710
1113
  # ===================
711
1114
  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."
@@ -833,21 +1236,24 @@ notification.quest_updated: "Quest Updated: Odd Jobs"
833
1236
  notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
834
1237
  notification.bought_drink: "Bought a drink (-5 gold)"
835
1238
  notification.bought_map: "Bought a map (-20 gold)"
836
- `), console.log(t.green(` ${f} Starter content created`)), console.log(""), console.log(t.dim(" Content includes:")), console.log(t.dim(" 2 locations (tavern, market)")), console.log(t.dim(" 2 characters (bartender, merchant)")), console.log(t.dim(" 1 item (old coin)")), console.log(t.dim(" 1 map (town)")), console.log(t.dim(" 1 quest with 3 stages")), console.log(t.dim(" 3 journal entries")), console.log(t.dim(" 4 dialogues (2 narrator intros, 2 NPC conversations)")), console.log(t.dim(" English locale with all strings")), await a(o(e, ".gitignore"), `node_modules
1239
+ `), 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 g(s(e, ".gitignore"), `node_modules
837
1240
  dist
838
1241
  .DS_Store
839
1242
  *.log
840
1243
  `);
841
1244
  }
842
- const p = new C();
843
- p.name("doodle").description(t.magenta("🐾 Doodle Engine") + t.dim(" — Narrative RPG development tools")).version("0.0.1");
844
- p.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
845
- await U(e);
1245
+ const E = new G();
1246
+ E.name("doodle").description(c.magenta("🐾 Doodle Engine") + c.dim(" — Narrative RPG development tools")).version("0.0.1");
1247
+ E.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
1248
+ await ne(e);
1249
+ });
1250
+ E.command("dev").description("Start development server with hot reload").action(async () => {
1251
+ await Y();
846
1252
  });
847
- p.command("dev").description("Start development server with hot reload").action(async () => {
848
- await j();
1253
+ E.command("build").description("Build game for production").action(async () => {
1254
+ await z();
849
1255
  });
850
- p.command("build").description("Build game for production").action(async () => {
851
- await M();
1256
+ E.command("validate").description("Validate game content").action(async () => {
1257
+ await Z();
852
1258
  });
853
- p.parse();
1259
+ E.parse();