@doodle-engine/cli 0.0.6 → 0.0.8

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,358 @@
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 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";
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));
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`
25
+ });
26
+ }
27
+ return a.push(...B(e, o)), a;
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}"`,
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,
39
+ message: `Start node "${e.startNode}" not found`,
40
+ suggestion: `Add a NODE ${e.startNode} or fix the startNode reference`
41
+ });
42
+ for (const r of e.nodes)
43
+ a.push(...F(r, n, o));
44
+ return a;
45
+ }
46
+ function F(e, o, a) {
47
+ const n = [];
48
+ if (e.next && !o.has(e.next) && n.push({
49
+ file: a,
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 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));
70
+ }
71
+ if (e.conditions)
72
+ for (const r of e.conditions)
73
+ n.push(...v(r, e.id, a));
74
+ if (e.effects)
75
+ for (const r of e.effects)
76
+ n.push(...T(r, e.id, a));
77
+ return n;
78
+ }
79
+ function v(e, o, a) {
80
+ const n = [];
81
+ 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;
123
+ }
124
+ function T(e, o, a) {
125
+ const n = [];
126
+ 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;
264
+ }
265
+ function B(e, o) {
266
+ const a = [], n = /* @__PURE__ */ new Set();
267
+ for (const t of Object.values(e.locales))
268
+ 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({
275
+ file: u,
276
+ message: `Localization key "${t}" not found in any locale file`,
277
+ suggestion: `Add "${m}: ..." to your locale files`
278
+ });
279
+ }
280
+ };
281
+ 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");
283
+ 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");
285
+ 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");
287
+ 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");
289
+ for (const l of t.stages)
290
+ r(l.description) && c(l.description, t.id, "quest");
291
+ }
292
+ 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");
294
+ for (const t of Object.values(e.dialogues))
295
+ for (const l of t.nodes) {
296
+ r(l.text) && c(l.text, t.id, "dialogue");
297
+ for (const d of l.choices)
298
+ r(d.text) && c(d.text, t.id, "dialogue");
299
+ }
300
+ return a;
301
+ }
302
+ function D(e) {
303
+ if (e.length === 0) {
304
+ console.log(i.green("✓ No validation errors"));
305
+ return;
306
+ }
307
+ console.log(i.red(`
308
+ ✗ Found ${e.length} validation error${e.length === 1 ? "" : "s"}:
309
+ `));
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();
312
+ }
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 = {
17
318
  name: "doodle-content-loader",
18
319
  configureServer(r) {
19
- r.middlewares.use("/api/content", async (n, l) => {
320
+ r.middlewares.use("/api/content", async (t, l) => {
20
321
  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" }));
322
+ const d = await Q(o);
323
+ l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(d));
324
+ } catch (d) {
325
+ console.error(i.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
25
326
  }
26
327
  });
27
- const c = I(o(s, "**/*"), {
328
+ const c = R(s(o, "**/*"), {
28
329
  ignored: /(^|[\/\\])\../,
29
330
  persistent: !0
30
331
  });
31
- c.on("change", (n) => {
32
- console.log(t.yellow(` ${q} Content changed: ${n}`)), r.ws.send({
332
+ c.on("change", async (t) => {
333
+ console.log(i.yellow(` ${J} Content changed: ${t}`)), await q(o), r.ws.send({
33
334
  type: "full-reload",
34
335
  path: "*"
35
336
  });
36
- }), c.on("add", (n) => {
37
- console.log(t.green(` ${H} Content added: ${n}`)), r.ws.send({
337
+ }), c.on("add", async (t) => {
338
+ console.log(i.green(` ${U} Content added: ${t}`)), await q(o), r.ws.send({
38
339
  type: "full-reload",
39
340
  path: "*"
40
341
  });
41
342
  });
42
343
  }
43
- }, h = await T({
344
+ }, n = await x({
44
345
  root: e,
45
- plugins: [O(), d],
346
+ plugins: [$(), a],
46
347
  server: {
47
348
  port: 3e3,
48
349
  open: !0
49
350
  }
50
351
  });
51
- await h.listen(), h.printUrls(), console.log(""), console.log(t.dim(` ${G} Watching content files for changes...`)), console.log("");
352
+ await n.listen(), n.printUrls(), console.log(""), console.log(i.dim(` ${L} Watching content files for changes...`)), console.log("");
52
353
  }
53
- async function x(e) {
54
- const s = {
354
+ async function Q(e) {
355
+ const o = {
55
356
  locations: {},
56
357
  characters: {},
57
358
  items: {},
@@ -61,8 +362,8 @@ async function x(e) {
61
362
  journalEntries: {},
62
363
  locales: {}
63
364
  };
64
- let d = null;
65
- const h = [
365
+ let a = null;
366
+ const n = [
66
367
  { dir: "locations", key: "locations" },
67
368
  { dir: "characters", key: "characters" },
68
369
  { dir: "items", key: "items" },
@@ -70,41 +371,41 @@ async function x(e) {
70
371
  { dir: "quests", key: "quests" },
71
372
  { dir: "journal", key: "journalEntries" }
72
373
  ];
73
- for (const { dir: r, key: c } of h) {
74
- const n = o(e, r);
374
+ for (const { dir: r, key: c } of n) {
375
+ const t = s(e, r);
75
376
  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);
377
+ const l = await y(t);
378
+ 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);
81
382
  }
82
383
  } catch {
83
384
  }
84
385
  }
85
386
  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 ?? {};
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 ?? {};
91
392
  }
92
393
  } catch {
93
394
  }
94
395
  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;
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;
100
401
  }
101
402
  } catch {
102
403
  }
103
404
  try {
104
- const r = o(e, "game.yaml"), c = await y(r, "utf-8");
105
- d = w(c);
405
+ const r = s(e, "game.yaml"), c = await f(r, "utf-8");
406
+ a = b(c);
106
407
  } catch {
107
- console.warn(t.yellow(" No game.yaml found, using defaults")), d = {
408
+ console.warn(i.yellow(" No game.yaml found, using defaults")), a = {
108
409
  startLocation: "tavern",
109
410
  startTime: { day: 1, hour: 8 },
110
411
  startFlags: {},
@@ -112,43 +413,214 @@ async function x(e) {
112
413
  startInventory: []
113
414
  };
114
415
  }
115
- return { registry: s, config: d };
416
+ return { registry: o, config: a };
116
417
  }
117
- async function M() {
118
- const e = process.cwd();
119
- console.log(`🐕🎮 Building Doodle Engine game...
120
- `);
418
+ async function q(e) {
121
419
  try {
122
- await N({
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);
424
+ }
425
+ }
426
+ async function V(e) {
427
+ const o = {
428
+ locations: {},
429
+ characters: {},
430
+ items: {},
431
+ maps: {},
432
+ dialogues: {},
433
+ quests: {},
434
+ journalEntries: {},
435
+ locales: {}
436
+ }, a = /* @__PURE__ */ new Map(), n = [
437
+ { dir: "locations", key: "locations" },
438
+ { dir: "characters", key: "characters" },
439
+ { dir: "items", key: "items" },
440
+ { dir: "maps", key: "maps" },
441
+ { dir: "quests", key: "quests" },
442
+ { dir: "journal", key: "journalEntries" }
443
+ ];
444
+ for (const { dir: r, key: c } of n) {
445
+ const t = s(e, r);
446
+ try {
447
+ const l = await y(t);
448
+ 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)));
452
+ }
453
+ } catch {
454
+ }
455
+ }
456
+ 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 ?? {};
462
+ }
463
+ } catch {
464
+ }
465
+ 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));
471
+ }
472
+ } catch {
473
+ }
474
+ return { registry: o, fileMap: a };
475
+ }
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..."));
479
+ 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));
482
+ } catch (a) {
483
+ console.error(i.red("Error loading content:"), a), process.exit(1);
484
+ }
485
+ console.log("");
486
+ try {
487
+ await G({
123
488
  root: e,
124
- plugins: [O()],
489
+ plugins: [$()],
125
490
  build: {
126
491
  outDir: "dist",
127
492
  emptyOutDir: !0
128
493
  }
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);
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("");
495
+ } catch (a) {
496
+ console.error(i.red("Build failed:"), a), process.exit(1);
135
497
  }
136
498
  }
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({
499
+ async function P(e) {
500
+ const o = {
501
+ locations: {},
502
+ characters: {},
503
+ items: {},
504
+ maps: {},
505
+ dialogues: {},
506
+ quests: {},
507
+ journalEntries: {},
508
+ locales: {}
509
+ }, a = /* @__PURE__ */ new Map(), n = [
510
+ { dir: "locations", key: "locations" },
511
+ { dir: "characters", key: "characters" },
512
+ { dir: "items", key: "items" },
513
+ { dir: "maps", key: "maps" },
514
+ { dir: "quests", key: "quests" },
515
+ { dir: "journal", key: "journalEntries" }
516
+ ];
517
+ for (const { dir: r, key: c } of n) {
518
+ const t = s(e, r);
519
+ 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)));
525
+ }
526
+ } catch {
527
+ }
528
+ }
529
+ 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 ?? {};
535
+ }
536
+ } catch {
537
+ }
538
+ 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));
544
+ }
545
+ } catch {
546
+ }
547
+ return { registry: o, fileMap: a };
548
+ }
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("");
552
+ 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);
557
+ }
558
+ }
559
+ async function z(e) {
560
+ const o = {
561
+ locations: {},
562
+ characters: {},
563
+ items: {},
564
+ maps: {},
565
+ dialogues: {},
566
+ quests: {},
567
+ journalEntries: {},
568
+ locales: {}
569
+ }, a = /* @__PURE__ */ new Map(), n = [
570
+ { dir: "locations", key: "locations" },
571
+ { dir: "characters", key: "characters" },
572
+ { dir: "items", key: "items" },
573
+ { dir: "maps", key: "maps" },
574
+ { dir: "quests", key: "quests" },
575
+ { dir: "journal", key: "journalEntries" }
576
+ ];
577
+ for (const { dir: r, key: c } of n) {
578
+ const t = s(e, r);
579
+ try {
580
+ const l = await y(t);
581
+ 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)));
585
+ }
586
+ } catch {
587
+ }
588
+ }
589
+ 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 ?? {};
595
+ }
596
+ } catch {
597
+ }
598
+ 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));
604
+ }
605
+ } catch {
606
+ }
607
+ return { registry: o, fileMap: a };
608
+ }
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({
142
614
  type: "confirm",
143
615
  name: "useDefaultRenderer",
144
616
  message: "Use default renderer?",
145
617
  initial: !0
146
618
  });
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("");
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("");
149
621
  }
150
- async function J(e, s, d) {
151
- const h = [
622
+ async function oe(e, o, a) {
623
+ const n = [
152
624
  "content/locations",
153
625
  "content/characters",
154
626
  "content/items",
@@ -166,12 +638,12 @@ async function J(e, s, d) {
166
638
  "assets/audio/voice",
167
639
  "src"
168
640
  ];
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("");
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("");
173
645
  const r = {
174
- name: s,
646
+ name: o,
175
647
  version: "0.1.0",
176
648
  type: "module",
177
649
  scripts: {
@@ -194,8 +666,8 @@ async function J(e, s, d) {
194
666
  vite: "^6.0.0"
195
667
  }
196
668
  };
197
- console.log(` ${B} ${t.bold("Writing project files...")}`), await a(
198
- o(e, "package.json"),
669
+ console.log(` ${Z} ${i.bold("Writing project files...")}`), await h(
670
+ s(e, "package.json"),
199
671
  JSON.stringify(r, null, 2)
200
672
  );
201
673
  const c = {
@@ -215,10 +687,10 @@ async function J(e, s, d) {
215
687
  },
216
688
  include: ["src"]
217
689
  };
218
- await a(
219
- o(e, "tsconfig.json"),
690
+ await h(
691
+ s(e, "tsconfig.json"),
220
692
  JSON.stringify(c, null, 2)
221
- ), await a(o(e, "index.html"), `<!doctype html>
693
+ ), await h(s(e, "index.html"), `<!doctype html>
222
694
  <html lang="en">
223
695
  <head>
224
696
  <meta charset="UTF-8" />
@@ -230,7 +702,7 @@ async function J(e, s, d) {
230
702
  <script type="module" src="/src/main.tsx"><\/script>
231
703
  </body>
232
704
  </html>
233
- `), await a(o(e, "src/main.tsx"), `import { StrictMode } from 'react'
705
+ `), await h(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
234
706
  import { createRoot } from 'react-dom/client'
235
707
  import { App } from './App'
236
708
  import './index.css'
@@ -241,8 +713,8 @@ createRoot(document.getElementById('root')!).render(
241
713
  </StrictMode>,
242
714
  )
243
715
  `);
244
- let i;
245
- d ? i = `import { useEffect, useState } from 'react'
716
+ let d;
717
+ a ? d = `import { useEffect, useState } from 'react'
246
718
  import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
247
719
  import { GameShell } from '@doodle-engine/react'
248
720
 
@@ -270,7 +742,7 @@ export function App() {
270
742
  />
271
743
  )
272
744
  }
273
- ` : i = `import { useEffect, useState } from 'react'
745
+ ` : d = `import { useEffect, useState } from 'react'
274
746
  import { Engine } from '@doodle-engine/core'
275
747
  import type { GameState, Snapshot } from '@doodle-engine/core'
276
748
  import { GameProvider, useGame } from '@doodle-engine/react'
@@ -361,7 +833,7 @@ function createEmptyState(): GameState {
361
833
  currentLocale: 'en',
362
834
  }
363
835
  }
364
- `, await a(o(e, "src/App.tsx"), i), await a(o(e, "src/index.css"), `body {
836
+ `, await h(s(e, "src/App.tsx"), d), await h(s(e, "src/index.css"), `body {
365
837
  margin: 0;
366
838
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
367
839
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -369,7 +841,7 @@ function createEmptyState(): GameState {
369
841
  -webkit-font-smoothing: antialiased;
370
842
  -moz-osx-font-smoothing: grayscale;
371
843
  }
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
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
373
845
  startLocation: tavern
374
846
  startTime:
375
847
  day: 1
@@ -380,40 +852,40 @@ startVariables:
380
852
  reputation: 0
381
853
  _drinksBought: 0
382
854
  startInventory: []
383
- `), await a(o(e, "content/locations/tavern.yaml"), `id: tavern
855
+ `), await h(s(e, "content/locations/tavern.yaml"), `id: tavern
384
856
  name: "@location.tavern.name"
385
857
  description: "@location.tavern.description"
386
858
  banner: ""
387
859
  music: ""
388
860
  ambient: ""
389
- `), await a(o(e, "content/locations/market.yaml"), `id: market
861
+ `), await h(s(e, "content/locations/market.yaml"), `id: market
390
862
  name: "@location.market.name"
391
863
  description: "@location.market.description"
392
864
  banner: ""
393
865
  music: ""
394
866
  ambient: ""
395
- `), await a(o(e, "content/characters/bartender.yaml"), `id: bartender
867
+ `), await h(s(e, "content/characters/bartender.yaml"), `id: bartender
396
868
  name: "@character.bartender.name"
397
869
  biography: "@character.bartender.bio"
398
870
  portrait: ""
399
871
  location: tavern
400
872
  dialogue: bartender_greeting
401
873
  stats: {}
402
- `), await a(o(e, "content/characters/merchant.yaml"), `id: merchant
874
+ `), await h(s(e, "content/characters/merchant.yaml"), `id: merchant
403
875
  name: "@character.merchant.name"
404
876
  biography: "@character.merchant.bio"
405
877
  portrait: ""
406
878
  location: market
407
879
  dialogue: merchant_intro
408
880
  stats: {}
409
- `), await a(o(e, "content/items/old_coin.yaml"), `id: old_coin
881
+ `), await h(s(e, "content/items/old_coin.yaml"), `id: old_coin
410
882
  name: "@item.old_coin.name"
411
883
  description: "@item.old_coin.description"
412
884
  icon: ""
413
885
  image: ""
414
886
  location: tavern
415
887
  stats: {}
416
- `), await a(o(e, "content/maps/town.yaml"), `id: town
888
+ `), await h(s(e, "content/maps/town.yaml"), `id: town
417
889
  name: "@map.town.name"
418
890
  image: ""
419
891
  scale: 1
@@ -424,7 +896,7 @@ locations:
424
896
  - id: market
425
897
  x: 300
426
898
  y: 150
427
- `), await a(o(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
899
+ `), await h(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
428
900
  name: "@quest.odd_jobs.name"
429
901
  description: "@quest.odd_jobs.description"
430
902
  stages:
@@ -434,19 +906,19 @@ stages:
434
906
  description: "@quest.odd_jobs.stage.talked_to_merchant"
435
907
  - id: complete
436
908
  description: "@quest.odd_jobs.stage.complete"
437
- `), await a(o(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
909
+ `), await h(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
438
910
  title: "@journal.tavern_discovery.title"
439
911
  text: "@journal.tavern_discovery.text"
440
912
  category: places
441
- `), await a(o(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
913
+ `), await h(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
442
914
  title: "@journal.odd_jobs_accepted.title"
443
915
  text: "@journal.odd_jobs_accepted.text"
444
916
  category: quests
445
- `), await a(o(e, "content/journal/market_square.yaml"), `id: market_square
917
+ `), await h(s(e, "content/journal/market_square.yaml"), `id: market_square
446
918
  title: "@journal.market_square.title"
447
919
  text: "@journal.market_square.text"
448
920
  category: places
449
- `), await a(o(e, "content/dialogues/tavern_intro.dlg"), `TRIGGER tavern
921
+ `), await h(s(e, "content/dialogues/tavern_intro.dlg"), `TRIGGER tavern
450
922
  REQUIRE notFlag seenTavernIntro
451
923
 
452
924
  NODE start
@@ -456,7 +928,7 @@ NODE start
456
928
  CHOICE @narrator.choice.look_around
457
929
  END dialogue
458
930
  END
459
- `), await a(o(e, "content/dialogues/market_intro.dlg"), `TRIGGER market
931
+ `), await h(s(e, "content/dialogues/market_intro.dlg"), `TRIGGER market
460
932
  REQUIRE notFlag seenMarketIntro
461
933
 
462
934
  NODE start
@@ -467,7 +939,7 @@ NODE start
467
939
  CHOICE @narrator.choice.look_around
468
940
  END dialogue
469
941
  END
470
- `), await a(o(e, "content/dialogues/bartender_greeting.dlg"), `NODE start
942
+ `), await h(s(e, "content/dialogues/bartender_greeting.dlg"), `NODE start
471
943
  BARTENDER: @bartender.greeting
472
944
 
473
945
  # Always available — ask for rumors (demonstrates: flag, relationship, journalEntry)
@@ -605,7 +1077,7 @@ NODE work_done
605
1077
  NODE farewell
606
1078
  BARTENDER: @bartender.farewell
607
1079
  END dialogue
608
- `), await a(o(e, "content/dialogues/merchant_intro.dlg"), `NODE start
1080
+ `), await h(s(e, "content/dialogues/merchant_intro.dlg"), `NODE start
609
1081
  MERCHANT: @merchant.greeting
610
1082
 
611
1083
  CHOICE @merchant.choice.browse_wares
@@ -705,7 +1177,7 @@ NODE about_market
705
1177
  NODE farewell
706
1178
  MERCHANT: @merchant.farewell
707
1179
  END dialogue
708
- `), await a(o(e, "content/locales/en.yaml"), `# ===================
1180
+ `), await h(s(e, "content/locales/en.yaml"), `# ===================
709
1181
  # Narrator Intros
710
1182
  # ===================
711
1183
  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 +1305,24 @@ notification.quest_updated: "Quest Updated: Odd Jobs"
833
1305
  notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
834
1306
  notification.bought_drink: "Bought a drink (-5 gold)"
835
1307
  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
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
837
1309
  dist
838
1310
  .DS_Store
839
1311
  *.log
840
1312
  `);
841
1313
  }
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);
1314
+ const k = new I();
1315
+ k.name("doodle").description(i.magenta("🐾 Doodle Engine") + i.dim(" — Narrative RPG development tools")).version("0.0.1");
1316
+ k.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
1317
+ await te(e);
1318
+ });
1319
+ k.command("dev").description("Start development server with hot reload").action(async () => {
1320
+ await W();
846
1321
  });
847
- p.command("dev").description("Start development server with hot reload").action(async () => {
848
- await j();
1322
+ k.command("build").description("Build game for production").action(async () => {
1323
+ await Y();
849
1324
  });
850
- p.command("build").description("Build game for production").action(async () => {
851
- await M();
1325
+ k.command("validate").description("Validate game content").action(async () => {
1326
+ await K();
852
1327
  });
853
- p.parse();
1328
+ k.parse();