@doodle-engine/cli 0.0.11 → 0.0.13

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,84 +1,84 @@
1
1
  #!/usr/bin/env node
2
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 m } 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));
3
+ import { crayon as i } from "crayon.js";
4
+ import { createServer as $, build as H } from "vite";
5
+ import j from "@vitejs/plugin-react";
6
+ import { watch as M } from "chokidar";
7
+ import { readdir as p, readFile as f, mkdir as D, writeFile as k } from "fs/promises";
8
+ import { join as d, extname as g, relative as _, dirname as F } from "path";
9
+ import { parse as y } from "yaml";
10
+ import { parseDialogue as E } from "@doodle-engine/core";
11
+ import P from "prompts";
12
+ function I(n, o) {
13
+ const r = [];
14
+ for (const a of Object.values(n.dialogues)) {
15
+ const t = o.get(a.id) || `dialogue:${a.id}`;
16
+ r.push(...B(a, t));
17
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,
18
+ for (const a of Object.values(n.characters))
19
+ if (a.dialogue && !n.dialogues[a.dialogue]) {
20
+ const t = o.get(a.id) || `character:${a.id}`;
21
+ r.push({
22
+ file: t,
23
23
  message: `Character "${a.id}" references non-existent dialogue "${a.dialogue}"`,
24
24
  suggestion: `Create dialogue "${a.dialogue}" or fix the reference`
25
25
  });
26
26
  }
27
- return i.push(...J(e, n)), i;
27
+ return r.push(...J(n, o)), r;
28
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}"`,
29
+ function B(n, o) {
30
+ const r = [], a = /* @__PURE__ */ new Set();
31
+ for (const t of n.nodes)
32
+ a.has(t.id) && r.push({
33
+ file: o,
34
+ message: `Duplicate node ID "${t.id}"`,
35
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`
36
+ }), a.add(t.id);
37
+ a.has(n.startNode) || r.push({
38
+ file: o,
39
+ message: `Start node "${n.startNode}" not found`,
40
+ suggestion: `Add a NODE ${n.startNode} or fix the startNode reference`
41
41
  });
42
- for (const o of e.nodes)
43
- i.push(...B(o, a, n));
44
- return i;
42
+ for (const t of n.nodes)
43
+ r.push(...L(t, a, o));
44
+ return r;
45
45
  }
46
- function B(e, n, i) {
46
+ function L(n, o, r) {
47
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));
48
+ if (n.next && !o.has(n.next) && a.push({
49
+ file: r,
50
+ message: `Node "${n.id}" GOTO "${n.next}" points to non-existent node`,
51
+ suggestion: `Add NODE ${n.next} or fix the GOTO target`
52
+ }), n.conditionalNext)
53
+ for (const t of n.conditionalNext)
54
+ o.has(t.next) || a.push({
55
+ file: r,
56
+ message: `Node "${n.id}" IF block GOTO "${t.next}" points to non-existent node`,
57
+ suggestion: `Add NODE ${t.next} or fix the GOTO target`
58
+ }), a.push(...v(t.condition, n.id, r));
59
+ for (const t of n.choices) {
60
+ if (!t.effects?.some(
61
+ (e) => e.type === "endDialogue" || e.type === "goToLocation"
62
+ ) && !o.has(t.next) && a.push({
63
+ file: r,
64
+ message: `Node "${n.id}" choice "${t.id}" GOTO "${t.next}" points to non-existent node`,
65
+ suggestion: `Add NODE ${t.next} or fix the GOTO target`
66
+ }), t.conditions)
67
+ for (const e of t.conditions)
68
+ a.push(...v(e, n.id, r));
69
+ if (t.effects)
70
+ for (const e of t.effects)
71
+ a.push(...C(e, n.id, r));
72
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));
73
+ if (n.conditions)
74
+ for (const t of n.conditions)
75
+ a.push(...v(t, n.id, r));
76
+ if (n.effects)
77
+ for (const t of n.effects)
78
+ a.push(...C(t, n.id, r));
79
79
  return a;
80
80
  }
81
- const L = {
81
+ const U = {
82
82
  hasFlag: ["flag"],
83
83
  notFlag: ["flag"],
84
84
  hasItem: ["itemId"],
@@ -92,8 +92,9 @@ const L = {
92
92
  characterInParty: ["characterId"],
93
93
  relationshipAbove: ["characterId", "value"],
94
94
  relationshipBelow: ["characterId", "value"],
95
- itemAt: ["itemId", "locationId"]
96
- }, U = {
95
+ itemAt: ["itemId", "locationId"],
96
+ roll: ["min", "max", "threshold"]
97
+ }, W = {
97
98
  setFlag: ["flag"],
98
99
  clearFlag: ["flag"],
99
100
  setVariable: ["variable", "value"],
@@ -119,140 +120,141 @@ const L = {
119
120
  playSound: ["sound"],
120
121
  notify: ["message"],
121
122
  playVideo: ["file"],
122
- showInterlude: ["interludeId"]
123
+ showInterlude: ["interludeId"],
124
+ roll: ["variable", "min", "max"]
123
125
  };
124
- function O(e, n, i) {
126
+ function v(n, o, r) {
125
127
  const a = [];
126
- if (!e.type)
128
+ if (!n.type)
127
129
  return a.push({
128
- file: i,
129
- message: `Node "${n}" has condition with missing type`
130
+ file: r,
131
+ message: `Node "${o}" has condition with missing type`
130
132
  }), 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`
133
+ if (n.type === "timeIs")
134
+ return n.hour === void 0 && n.day === void 0 && a.push({
135
+ file: r,
136
+ message: `Node "${o}" condition "timeIs" must have at least one of "hour" or "day" argument`
135
137
  }), a;
136
- const o = L[e.type];
137
- if (!o)
138
+ const t = U[n.type];
139
+ if (!t)
138
140
  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`
141
+ for (const s of t)
142
+ (n[s] === void 0 || n[s] === null || n[s] === "") && a.push({
143
+ file: r,
144
+ message: `Node "${o}" condition "${n.type}" missing required "${s}" argument`
143
145
  });
144
146
  return a;
145
147
  }
146
- function C(e, n, i) {
148
+ function C(n, o, r) {
147
149
  const a = [];
148
- if (!e.type)
150
+ if (!n.type)
149
151
  return a.push({
150
- file: i,
151
- message: `Node "${n}" has effect with missing type`
152
+ file: r,
153
+ message: `Node "${o}" has effect with missing type`
152
154
  }), a;
153
- const o = U[e.type];
154
- if (!o)
155
+ const t = W[n.type];
156
+ if (!t)
155
157
  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`
158
+ for (const s of t)
159
+ (n[s] === void 0 || n[s] === null || n[s] === "") && a.push({
160
+ file: r,
161
+ message: `Node "${o}" effect "${n.type}" missing required "${s}" argument`
160
162
  });
161
163
  return a;
162
164
  }
163
- function J(e, n) {
164
- const i = [], a = /* @__PURE__ */ new Set();
165
- for (const t of Object.values(e.locales))
166
- for (const l of Object.keys(t))
167
- a.add(l);
168
- const o = (t) => t.startsWith("@"), r = (t, l, d) => {
169
- const u = t.slice(1);
165
+ function J(n, o) {
166
+ const r = [], a = /* @__PURE__ */ new Set();
167
+ for (const e of Object.values(n.locales))
168
+ for (const c of Object.keys(e))
169
+ a.add(c);
170
+ const t = (e) => e.startsWith("@"), s = (e, c, l) => {
171
+ const u = e.slice(1);
170
172
  if (!a.has(u)) {
171
- const h = n.get(l) || `${d}:${l}`;
172
- i.push({
173
+ const h = o.get(c) || `${l}:${c}`;
174
+ r.push({
173
175
  file: h,
174
- message: `Localization key "${t}" not found in any locale file`,
176
+ message: `Localization key "${e}" not found in any locale file`,
175
177
  suggestion: `Add "${u}: ..." to your locale files`
176
178
  });
177
179
  }
178
180
  };
179
- for (const t of Object.values(e.locations))
180
- o(t.name) && r(t.name, t.id, "location"), o(t.description) && r(t.description, t.id, "location");
181
- for (const t of Object.values(e.characters))
182
- o(t.name) && r(t.name, t.id, "character"), o(t.biography) && r(t.biography, t.id, "character");
183
- for (const t of Object.values(e.items))
184
- o(t.name) && r(t.name, t.id, "item"), o(t.description) && r(t.description, t.id, "item");
185
- for (const t of Object.values(e.quests)) {
186
- o(t.name) && r(t.name, t.id, "quest"), o(t.description) && r(t.description, t.id, "quest");
187
- for (const l of t.stages)
188
- o(l.description) && r(l.description, t.id, "quest");
181
+ for (const e of Object.values(n.locations))
182
+ t(e.name) && s(e.name, e.id, "location"), t(e.description) && s(e.description, e.id, "location");
183
+ for (const e of Object.values(n.characters))
184
+ t(e.name) && s(e.name, e.id, "character"), t(e.biography) && s(e.biography, e.id, "character");
185
+ for (const e of Object.values(n.items))
186
+ t(e.name) && s(e.name, e.id, "item"), t(e.description) && s(e.description, e.id, "item");
187
+ for (const e of Object.values(n.quests)) {
188
+ t(e.name) && s(e.name, e.id, "quest"), t(e.description) && s(e.description, e.id, "quest");
189
+ for (const c of e.stages)
190
+ t(c.description) && s(c.description, e.id, "quest");
189
191
  }
190
- for (const t of Object.values(e.journalEntries))
191
- o(t.title) && r(t.title, t.id, "journal"), o(t.text) && r(t.text, t.id, "journal");
192
- for (const t of Object.values(e.dialogues))
193
- for (const l of t.nodes) {
194
- o(l.text) && r(l.text, t.id, "dialogue");
195
- for (const d of l.choices)
196
- o(d.text) && r(d.text, t.id, "dialogue");
192
+ for (const e of Object.values(n.journalEntries))
193
+ t(e.title) && s(e.title, e.id, "journal"), t(e.text) && s(e.text, e.id, "journal");
194
+ for (const e of Object.values(n.dialogues))
195
+ for (const c of e.nodes) {
196
+ t(c.text) && s(c.text, e.id, "dialogue");
197
+ for (const l of c.choices)
198
+ t(l.text) && s(l.text, e.id, "dialogue");
197
199
  }
198
- for (const t of Object.values(e.interludes))
199
- o(t.text) && r(t.text, t.id, "interlude");
200
- return i;
200
+ for (const e of Object.values(n.interludes))
201
+ t(e.text) && s(e.text, e.id, "interlude");
202
+ return r;
201
203
  }
202
- function T(e) {
203
- if (e.length === 0) {
204
- console.log(c.green("✓ No validation errors"));
204
+ function N(n) {
205
+ if (n.length === 0) {
206
+ console.log(i.green("✓ No validation errors"));
205
207
  return;
206
208
  }
207
- console.log(c.red(`
208
- ✗ Found ${e.length} validation error${e.length === 1 ? "" : "s"}:
209
+ console.log(i.red(`
210
+ ✗ Found ${n.length} validation error${n.length === 1 ? "" : "s"}:
209
211
  `));
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();
212
+ for (const o of n)
213
+ 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();
212
214
  }
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 = {
215
+ const R = "ðŸū", Q = "âœĻ", Y = "✏ïļ", V = "➕";
216
+ async function K() {
217
+ const n = process.cwd(), o = d(n, "content");
218
+ console.log(""), console.log(i.bold.magenta(` ${R} Doodle Engine Dev Server ${R}`)), console.log("");
219
+ const r = {
218
220
  name: "doodle-content-loader",
219
- configureServer(o) {
220
- o.middlewares.use("/api/content", async (t, l) => {
221
+ configureServer(t) {
222
+ t.middlewares.use("/api/content", async (e, c) => {
221
223
  try {
222
- const d = await P(n);
223
- l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(d));
224
- } catch (d) {
225
- console.error(c.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
224
+ const l = await z(o);
225
+ c.setHeader("Content-Type", "application/json"), c.end(JSON.stringify(l));
226
+ } catch (l) {
227
+ console.error(i.red(" Error loading content:"), l), c.statusCode = 500, c.end(JSON.stringify({ error: "Failed to load content" }));
226
228
  }
227
229
  });
228
- const r = H(n, {
230
+ const s = M(o, {
229
231
  ignored: /(^|[\/\\])\../,
230
232
  persistent: !0
231
233
  });
232
- r.on("change", async (t) => {
233
- console.log(c.yellow(` ${Q} Content changed: ${t}`)), await S(n), o.ws.send({
234
+ s.on("change", async (e) => {
235
+ console.log(i.yellow(` ${Y} Content changed: ${e}`)), await A(o), t.ws.send({
234
236
  type: "full-reload",
235
237
  path: "*"
236
238
  });
237
- }), r.on("add", async (t) => {
238
- console.log(c.green(` ${V} Content added: ${t}`)), await S(n), o.ws.send({
239
+ }), s.on("add", async (e) => {
240
+ console.log(i.green(` ${V} Content added: ${e}`)), await A(o), t.ws.send({
239
241
  type: "full-reload",
240
242
  path: "*"
241
243
  });
242
244
  });
243
245
  }
244
- }, a = await j({
245
- root: e,
246
- plugins: [A(), i],
246
+ }, a = await $({
247
+ root: n,
248
+ plugins: [j(), r],
247
249
  server: {
248
250
  port: 3e3,
249
251
  open: !0
250
252
  }
251
253
  });
252
- await a.listen(), a.printUrls(), console.log(""), console.log(c.dim(` ${W} Watching content files for changes...`)), console.log("");
254
+ await a.listen(), a.printUrls(), console.log(""), console.log(i.dim(` ${Q} Watching content files for changes...`)), console.log("");
253
255
  }
254
- async function P(e) {
255
- const n = {
256
+ async function z(n) {
257
+ const o = {
256
258
  locations: {},
257
259
  characters: {},
258
260
  items: {},
@@ -263,7 +265,7 @@ async function P(e) {
263
265
  interludes: {},
264
266
  locales: {}
265
267
  };
266
- let i = null;
268
+ let r = null;
267
269
  const a = [
268
270
  { dir: "locations", key: "locations" },
269
271
  { dir: "characters", key: "characters" },
@@ -273,41 +275,41 @@ async function P(e) {
273
275
  { dir: "journal", key: "journalEntries" },
274
276
  { dir: "interludes", key: "interludes" }
275
277
  ];
276
- for (const { dir: o, key: r } of a) {
277
- const t = s(e, o);
278
+ for (const { dir: t, key: s } of a) {
279
+ const e = d(n, t);
278
280
  try {
279
- const l = await y(t);
280
- for (const d of l)
281
- if (f(d) === ".yaml" || f(d) === ".yml") {
282
- const u = s(t, d), h = await p(u, "utf-8"), g = b(h);
283
- g && g.id && (n[r][g.id] = g);
281
+ const c = await p(e);
282
+ for (const l of c)
283
+ if (g(l) === ".yaml" || g(l) === ".yml") {
284
+ const u = d(e, l), h = await f(u, "utf-8"), m = y(h);
285
+ m && m.id && (o[s][m.id] = m);
284
286
  }
285
287
  } catch {
286
288
  }
287
289
  }
288
290
  try {
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"), u = b(d), h = t.replace(/\.(yaml|yml)$/, "");
293
- n.locales[h] = u ?? {};
291
+ const t = d(n, "locales"), s = await p(t);
292
+ for (const e of s)
293
+ if (g(e) === ".yaml" || g(e) === ".yml") {
294
+ const c = d(t, e), l = await f(c, "utf-8"), u = y(l), h = e.replace(/\.(yaml|yml)$/, "");
295
+ o.locales[h] = u ?? {};
294
296
  }
295
297
  } catch {
296
298
  }
297
299
  try {
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"), u = t.replace(".dlg", ""), h = v(d, u);
302
- n.dialogues[h.id] = h;
300
+ const t = d(n, "dialogues"), s = await p(t);
301
+ for (const e of s)
302
+ if (g(e) === ".dlg") {
303
+ const c = d(t, e), l = await f(c, "utf-8"), u = e.replace(".dlg", ""), h = E(l, u);
304
+ o.dialogues[h.id] = h;
303
305
  }
304
306
  } catch {
305
307
  }
306
308
  try {
307
- const o = s(e, "game.yaml"), r = await p(o, "utf-8");
308
- i = b(r);
309
+ const t = d(n, "game.yaml"), s = await f(t, "utf-8");
310
+ r = y(s);
309
311
  } catch {
310
- console.warn(c.yellow(" No game.yaml found, using defaults")), i = {
312
+ console.warn(i.yellow(" No game.yaml found, using defaults")), r = {
311
313
  startLocation: "tavern",
312
314
  startTime: { day: 1, hour: 8 },
313
315
  startFlags: {},
@@ -315,18 +317,18 @@ async function P(e) {
315
317
  startInventory: []
316
318
  };
317
319
  }
318
- return { registry: n, config: i };
320
+ return { registry: o, config: r };
319
321
  }
320
- async function S(e) {
322
+ async function A(n) {
321
323
  try {
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);
324
+ const { registry: o, fileMap: r } = await X(n), a = I(o, r);
325
+ a.length > 0 && (console.log(""), N(a), console.log(""));
326
+ } catch (o) {
327
+ console.error(i.red(" Error running validation:"), o);
326
328
  }
327
329
  }
328
- async function K(e) {
329
- const n = {
330
+ async function X(n) {
331
+ const o = {
330
332
  locations: {},
331
333
  characters: {},
332
334
  items: {},
@@ -336,7 +338,7 @@ async function K(e) {
336
338
  journalEntries: {},
337
339
  interludes: {},
338
340
  locales: {}
339
- }, i = /* @__PURE__ */ new Map(), a = [
341
+ }, r = /* @__PURE__ */ new Map(), a = [
340
342
  { dir: "locations", key: "locations" },
341
343
  { dir: "characters", key: "characters" },
342
344
  { dir: "items", key: "items" },
@@ -345,66 +347,66 @@ async function K(e) {
345
347
  { dir: "journal", key: "journalEntries" },
346
348
  { dir: "interludes", key: "interludes" }
347
349
  ];
348
- for (const { dir: o, key: r } of a) {
349
- const t = s(e, o);
350
+ for (const { dir: t, key: s } of a) {
351
+ const e = d(n, t);
350
352
  try {
351
- const l = await y(t);
352
- for (const d of l)
353
- if (f(d) === ".yaml" || f(d) === ".yml") {
354
- const u = s(t, d), h = await p(u, "utf-8"), g = b(h);
355
- g && g.id && (n[r][g.id] = g, i.set(g.id, w(process.cwd(), u)));
353
+ const c = await p(e);
354
+ for (const l of c)
355
+ if (g(l) === ".yaml" || g(l) === ".yml") {
356
+ const u = d(e, l), h = await f(u, "utf-8"), m = y(h);
357
+ m && m.id && (o[s][m.id] = m, r.set(m.id, _(process.cwd(), u)));
356
358
  }
357
359
  } catch {
358
360
  }
359
361
  }
360
362
  try {
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"), u = b(d), h = t.replace(/\.(yaml|yml)$/, "");
365
- n.locales[h] = u ?? {};
363
+ const t = d(n, "locales"), s = await p(t);
364
+ for (const e of s)
365
+ if (g(e) === ".yaml" || g(e) === ".yml") {
366
+ const c = d(t, e), l = await f(c, "utf-8"), u = y(l), h = e.replace(/\.(yaml|yml)$/, "");
367
+ o.locales[h] = u ?? {};
366
368
  }
367
369
  } catch {
368
370
  }
369
371
  try {
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"), u = t.replace(".dlg", ""), h = v(d, u);
374
- n.dialogues[h.id] = h, i.set(h.id, w(process.cwd(), l));
372
+ const t = d(n, "dialogues"), s = await p(t);
373
+ for (const e of s)
374
+ if (g(e) === ".dlg") {
375
+ const c = d(t, e), l = await f(c, "utf-8"), u = e.replace(".dlg", ""), h = E(l, u);
376
+ o.dialogues[h.id] = h, r.set(h.id, _(process.cwd(), c));
375
377
  }
376
378
  } catch {
377
379
  }
378
- return { registry: n, fileMap: i };
380
+ return { registry: o, fileMap: r };
379
381
  }
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;
382
+ async function Z() {
383
+ const n = process.cwd(), o = d(n, "content");
384
+ console.log(""), console.log(i.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(i.dim("Validating content..."));
385
+ let r;
384
386
  try {
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 };
387
+ const { registry: a, fileMap: t, config: s } = await ee(o), e = I(a, t);
388
+ N(e), e.length > 0 && (console.log(i.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), r = { registry: a, config: s };
387
389
  } catch (a) {
388
- console.error(c.red("Error loading content:"), a), process.exit(1);
390
+ console.error(i.red("Error loading content:"), a), process.exit(1);
389
391
  }
390
392
  console.log("");
391
393
  try {
392
- await $({
393
- root: e,
394
- plugins: [A()],
394
+ await H({
395
+ root: n,
396
+ plugins: [j()],
395
397
  build: {
396
398
  outDir: "dist",
397
399
  emptyOutDir: !0
398
400
  }
399
401
  });
400
- const a = s(e, "dist", "api");
401
- await R(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("");
402
+ const a = d(n, "dist", "api");
403
+ await D(a, { recursive: !0 }), await k(d(a, "content"), JSON.stringify(r)), 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("");
402
404
  } catch (a) {
403
- console.error(c.red("Build failed:"), a), process.exit(1);
405
+ console.error(i.red("Build failed:"), a), process.exit(1);
404
406
  }
405
407
  }
406
- async function X(e) {
407
- const n = {
408
+ async function ee(n) {
409
+ const o = {
408
410
  locations: {},
409
411
  characters: {},
410
412
  items: {},
@@ -414,7 +416,7 @@ async function X(e) {
414
416
  journalEntries: {},
415
417
  interludes: {},
416
418
  locales: {}
417
- }, i = /* @__PURE__ */ new Map(), a = [
419
+ }, r = /* @__PURE__ */ new Map(), a = [
418
420
  { dir: "locations", key: "locations" },
419
421
  { dir: "characters", key: "characters" },
420
422
  { dir: "items", key: "items" },
@@ -423,57 +425,57 @@ async function X(e) {
423
425
  { dir: "journal", key: "journalEntries" },
424
426
  { dir: "interludes", key: "interludes" }
425
427
  ];
426
- for (const { dir: r, key: t } of a) {
427
- const l = s(e, r);
428
+ for (const { dir: s, key: e } of a) {
429
+ const c = d(n, s);
428
430
  try {
429
- const d = await y(l);
430
- for (const u of d)
431
- if (f(u) === ".yaml" || f(u) === ".yml") {
432
- const h = s(l, u), g = await p(h, "utf-8"), E = b(g);
433
- E && E.id && (n[t][E.id] = E, i.set(E.id, w(process.cwd(), h)));
431
+ const l = await p(c);
432
+ for (const u of l)
433
+ if (g(u) === ".yaml" || g(u) === ".yml") {
434
+ const h = d(c, u), m = await f(h, "utf-8"), w = y(m);
435
+ w && w.id && (o[e][w.id] = w, r.set(w.id, _(process.cwd(), h)));
434
436
  }
435
437
  } catch {
436
438
  }
437
439
  }
438
440
  try {
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), u = await p(d, "utf-8"), h = b(u), g = l.replace(/\.(yaml|yml)$/, "");
443
- n.locales[g] = h ?? {};
441
+ const s = d(n, "locales"), e = await p(s);
442
+ for (const c of e)
443
+ if (g(c) === ".yaml" || g(c) === ".yml") {
444
+ const l = d(s, c), u = await f(l, "utf-8"), h = y(u), m = c.replace(/\.(yaml|yml)$/, "");
445
+ o.locales[m] = h ?? {};
444
446
  }
445
447
  } catch {
446
448
  }
447
449
  try {
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), u = await p(d, "utf-8"), h = l.replace(".dlg", ""), g = v(u, h);
452
- n.dialogues[g.id] = g, i.set(g.id, w(process.cwd(), d));
450
+ const s = d(n, "dialogues"), e = await p(s);
451
+ for (const c of e)
452
+ if (g(c) === ".dlg") {
453
+ const l = d(s, c), u = await f(l, "utf-8"), h = c.replace(".dlg", ""), m = E(u, h);
454
+ o.dialogues[m.id] = m, r.set(m.id, _(process.cwd(), l));
453
455
  }
454
456
  } catch {
455
457
  }
456
- let o = null;
458
+ let t = null;
457
459
  try {
458
- const r = s(e, "game.yaml"), t = await p(r, "utf-8");
459
- o = b(t);
460
+ const s = d(n, "game.yaml"), e = await f(s, "utf-8");
461
+ t = y(e);
460
462
  } catch {
461
- o = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
463
+ t = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
462
464
  }
463
- return { registry: n, fileMap: i, config: o };
465
+ return { registry: o, fileMap: r, config: t };
464
466
  }
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("");
467
+ async function ne() {
468
+ const n = process.cwd(), o = d(n, "content");
469
+ console.log(""), console.log(i.bold.magenta("ðŸū Validating Doodle Engine content...")), console.log("");
468
470
  try {
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);
471
+ const { registry: r, fileMap: a } = await te(o), t = I(r, a);
472
+ N(t), t.length > 0 && process.exit(1);
473
+ } catch (r) {
474
+ console.error(i.red("Error loading content:"), r), process.exit(1);
473
475
  }
474
476
  }
475
- async function ee(e) {
476
- const n = {
477
+ async function te(n) {
478
+ const o = {
477
479
  locations: {},
478
480
  characters: {},
479
481
  items: {},
@@ -482,7 +484,7 @@ async function ee(e) {
482
484
  quests: {},
483
485
  journalEntries: {},
484
486
  locales: {}
485
- }, i = /* @__PURE__ */ new Map(), a = [
487
+ }, r = /* @__PURE__ */ new Map(), a = [
486
488
  { dir: "locations", key: "locations" },
487
489
  { dir: "characters", key: "characters" },
488
490
  { dir: "items", key: "items" },
@@ -490,124 +492,43 @@ async function ee(e) {
490
492
  { dir: "quests", key: "quests" },
491
493
  { dir: "journal", key: "journalEntries" }
492
494
  ];
493
- for (const { dir: o, key: r } of a) {
494
- const t = s(e, o);
495
+ for (const { dir: t, key: s } of a) {
496
+ const e = d(n, t);
495
497
  try {
496
- const l = await y(t);
497
- for (const d of l)
498
- if (f(d) === ".yaml" || f(d) === ".yml") {
499
- const u = s(t, d), h = await p(u, "utf-8"), g = b(h);
500
- g && g.id && (n[r][g.id] = g, i.set(g.id, w(process.cwd(), u)));
498
+ const c = await p(e);
499
+ for (const l of c)
500
+ if (g(l) === ".yaml" || g(l) === ".yml") {
501
+ const u = d(e, l), h = await f(u, "utf-8"), m = y(h);
502
+ m && m.id && (o[s][m.id] = m, r.set(m.id, _(process.cwd(), u)));
501
503
  }
502
504
  } catch {
503
505
  }
504
506
  }
505
507
  try {
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"), u = b(d), h = t.replace(/\.(yaml|yml)$/, "");
510
- n.locales[h] = u ?? {};
508
+ const t = d(n, "locales"), s = await p(t);
509
+ for (const e of s)
510
+ if (g(e) === ".yaml" || g(e) === ".yml") {
511
+ const c = d(t, e), l = await f(c, "utf-8"), u = y(l), h = e.replace(/\.(yaml|yml)$/, "");
512
+ o.locales[h] = u ?? {};
511
513
  }
512
514
  } catch {
513
515
  }
514
516
  try {
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"), u = t.replace(".dlg", ""), h = v(d, u);
519
- n.dialogues[h.id] = h, i.set(h.id, w(process.cwd(), l));
517
+ const t = d(n, "dialogues"), s = await p(t);
518
+ for (const e of s)
519
+ if (g(e) === ".dlg") {
520
+ const c = d(t, e), l = await f(c, "utf-8"), u = e.replace(".dlg", ""), h = E(l, u);
521
+ o.dialogues[h.id] = h, r.set(h.id, _(process.cwd(), c));
520
522
  }
521
523
  } catch {
522
524
  }
523
- return { registry: n, fileMap: i };
524
- }
525
- const D = "ðŸū", te = "🐕", x = "ðŸĶī", 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({
530
- type: "confirm",
531
- name: "useDefaultRenderer",
532
- message: "Use default renderer?",
533
- initial: !0
534
- });
535
- i === void 0 && (console.log(c.yellow(`
536
- ${x} 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("");
525
+ return { registry: o, fileMap: r };
537
526
  }
538
- async function re(e, n, i) {
539
- const a = [
540
- "content/locations",
541
- "content/characters",
542
- "content/items",
543
- "content/dialogues",
544
- "content/quests",
545
- "content/journal",
546
- "content/interludes",
547
- "content/locales",
548
- "content/maps",
549
- "assets/images/banners",
550
- "assets/images/portraits",
551
- "assets/images/items",
552
- "assets/images/maps",
553
- "assets/audio/music",
554
- "assets/audio/sfx",
555
- "assets/audio/voice",
556
- "src"
557
- ];
558
- console.log(` ${q} ${c.bold("Creating directories...")}`);
559
- for (const h of a)
560
- await R(s(e, h), { recursive: !0 });
561
- console.log(c.green(` ${_} Directories created`)), console.log("");
562
- const o = {
563
- name: n,
564
- version: "0.1.0",
565
- type: "module",
566
- scripts: {
567
- dev: "doodle dev",
568
- build: "doodle build",
569
- preview: "vite preview"
570
- },
571
- dependencies: {
572
- "@doodle-engine/core": "latest",
573
- "@doodle-engine/react": "latest",
574
- react: "^19.0.0",
575
- "react-dom": "^19.0.0"
576
- },
577
- devDependencies: {
578
- "@doodle-engine/cli": "latest",
579
- "@types/react": "^19.0.0",
580
- "@types/react-dom": "^19.0.0",
581
- "@vitejs/plugin-react": "^4.3.0",
582
- typescript: "^5.7.0",
583
- vite: "^6.0.0"
584
- }
585
- };
586
- console.log(` ${oe} ${c.bold("Writing project files...")}`), await m(
587
- s(e, "package.json"),
588
- JSON.stringify(o, null, 2)
589
- );
590
- const r = {
591
- compilerOptions: {
592
- target: "ES2024",
593
- lib: ["ES2024", "DOM", "DOM.Iterable"],
594
- module: "ESNext",
595
- moduleResolution: "bundler",
596
- jsx: "react-jsx",
597
- strict: !0,
598
- skipLibCheck: !0,
599
- esModuleInterop: !0,
600
- forceConsistentCasingInFileNames: !0,
601
- resolveJsonModule: !0,
602
- isolatedModules: !0,
603
- noEmit: !0
604
- },
605
- include: ["src"]
606
- };
607
- await m(
608
- s(e, "tsconfig.json"),
609
- JSON.stringify(r, null, 2)
610
- ), await m(s(e, "index.html"), `<!doctype html>
527
+ const oe = `node_modules
528
+ dist
529
+ .DS_Store
530
+ *.log
531
+ `, ae = `<!doctype html>
611
532
  <html lang="en">
612
533
  <head>
613
534
  <meta charset="UTF-8" />
@@ -619,296 +540,40 @@ async function re(e, n, i) {
619
540
  <script type="module" src="/src/main.tsx"><\/script>
620
541
  </body>
621
542
  </html>
622
- `), await m(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
623
- import { createRoot } from 'react-dom/client'
624
- import { App } from './App'
625
- import './index.css'
626
-
627
- createRoot(document.getElementById('root')!).render(
628
- <StrictMode>
629
- <App />
630
- </StrictMode>,
631
- )
632
- `);
633
- let d;
634
- i ? d = `import { useEffect, useState } from 'react'
635
- import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
636
- import { GameShell, LoadingScreen } from '@doodle-engine/react'
637
-
638
- export function App() {
639
- const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
640
-
641
- useEffect(() => {
642
- fetch('/api/content')
643
- .then(res => res.json())
644
- .then(data => setContent({ registry: data.registry, config: data.config }))
645
- }, [])
646
-
647
- if (!content) {
648
- return <LoadingScreen />
649
- }
650
-
651
- return (
652
- <GameShell
653
- registry={content.registry}
654
- config={content.config}
655
- title="My Doodle Game"
656
- subtitle="A text-based adventure"
657
- splashDuration={2000}
658
- availableLocales={[{ code: 'en', label: 'English' }]}
659
- devTools={import.meta.env.DEV}
660
- />
661
- )
662
- }
663
- ` : d = `import { useEffect, useState } from 'react'
664
- import { Engine } from '@doodle-engine/core'
665
- import type { GameState, Snapshot } from '@doodle-engine/core'
666
- import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
667
-
668
- export function App() {
669
- const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
670
-
671
- useEffect(() => {
672
- fetch('/api/content')
673
- .then(res => res.json())
674
- .then(data => {
675
- const engine = new Engine(data.registry, createEmptyState())
676
- const snapshot = engine.newGame(data.config)
677
- setGame({ engine, snapshot })
678
- })
679
- }, [])
680
-
681
- if (!game) {
682
- return <LoadingScreen />
683
- }
684
-
685
- return (
686
- <GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
687
- <GameUI />
688
- </GameProvider>
689
- )
690
- }
691
-
692
- function GameUI() {
693
- const { snapshot, actions } = useGame()
694
-
695
- return (
696
- <div style={{ padding: '2rem', fontFamily: 'sans-serif', maxWidth: '800px', margin: '0 auto' }}>
697
- <h1>{snapshot.location.name}</h1>
698
- <p>{snapshot.location.description}</p>
699
-
700
- {snapshot.dialogue && (
701
- <div style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px', margin: '1rem 0' }}>
702
- <strong>{snapshot.dialogue.speakerName}:</strong>
703
- <p>{snapshot.dialogue.text}</p>
704
- {snapshot.choices.map(choice => (
705
- <button
706
- key={choice.id}
707
- onClick={() => actions.selectChoice(choice.id)}
708
- style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
709
- >
710
- {choice.text}
711
- </button>
712
- ))}
713
- </div>
714
- )}
715
-
716
- {!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
717
- <div>
718
- <h2>Characters here</h2>
719
- {snapshot.charactersHere.map(char => (
720
- <button
721
- key={char.id}
722
- onClick={() => actions.talkTo(char.id)}
723
- style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
724
- >
725
- Talk to {char.name}
726
- </button>
727
- ))}
728
- </div>
729
- )}
730
- </div>
731
- )
732
- }
733
-
734
- function createEmptyState(): GameState {
735
- return {
736
- currentLocation: '',
737
- currentTime: { day: 1, hour: 0 },
738
- flags: {},
739
- variables: {},
740
- inventory: [],
741
- questProgress: {},
742
- unlockedJournalEntries: [],
743
- playerNotes: [],
744
- dialogueState: null,
745
- characterState: {},
746
- itemLocations: {},
747
- mapEnabled: true,
748
- notifications: [],
749
- pendingSounds: [],
750
- pendingVideo: null,
751
- currentLocale: 'en',
752
- }
753
- }
754
- `, await m(s(e, "src/App.tsx"), d), await m(s(e, "src/index.css"), `body {
755
- margin: 0;
756
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
757
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
758
- sans-serif;
759
- -webkit-font-smoothing: antialiased;
760
- -moz-osx-font-smoothing: grayscale;
543
+ `, re = `{
544
+ "compilerOptions": {
545
+ "target": "ES2024",
546
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
547
+ "module": "ESNext",
548
+ "moduleResolution": "bundler",
549
+ "jsx": "react-jsx",
550
+ "strict": true,
551
+ "skipLibCheck": true,
552
+ "esModuleInterop": true,
553
+ "forceConsistentCasingInFileNames": true,
554
+ "resolveJsonModule": true,
555
+ "isolatedModules": true,
556
+ "noEmit": true
557
+ },
558
+ "include": ["src"]
761
559
  }
762
- `), console.log(c.green(` ${_} Source files created`)), console.log(""), console.log(` ${x} ${c.bold("Writing starter content...")}`), await m(s(e, "content/game.yaml"), `# Game Configuration
763
- startLocation: tavern
764
- startTime:
765
- day: 1
766
- hour: 8
767
- startFlags: {}
768
- startVariables:
769
- gold: 100
770
- reputation: 0
771
- _drinksBought: 0
772
- startInventory: []
773
- `), await m(s(e, "content/locations/tavern.yaml"), `id: tavern
774
- name: "@location.tavern.name"
775
- description: "@location.tavern.description"
776
- banner: ""
777
- music: ""
778
- ambient: ""
779
- `), await m(s(e, "content/locations/market.yaml"), `id: market
780
- name: "@location.market.name"
781
- description: "@location.market.description"
782
- banner: ""
783
- music: ""
784
- ambient: ""
785
- `), await m(s(e, "content/characters/bartender.yaml"), `id: bartender
560
+ `, se = `id: bartender
786
561
  name: "@character.bartender.name"
787
562
  biography: "@character.bartender.bio"
788
563
  portrait: ""
789
564
  location: tavern
790
565
  dialogue: bartender_greeting
791
566
  stats: {}
792
- `), await m(s(e, "content/characters/merchant.yaml"), `id: merchant
567
+ `, ie = `id: merchant
793
568
  name: "@character.merchant.name"
794
569
  biography: "@character.merchant.bio"
795
570
  portrait: ""
796
571
  location: market
797
572
  dialogue: merchant_intro
798
573
  stats: {}
799
- `), await m(s(e, "content/items/old_coin.yaml"), `id: old_coin
800
- name: "@item.old_coin.name"
801
- description: "@item.old_coin.description"
802
- icon: ""
803
- image: ""
804
- location: tavern
805
- stats: {}
806
- `), await m(s(e, "content/maps/town.yaml"), `id: town
807
- name: "@map.town.name"
808
- image: ""
809
- scale: 1
810
- locations:
811
- - id: tavern
812
- x: 100
813
- y: 200
814
- - id: market
815
- x: 300
816
- y: 150
817
- `), await m(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
818
- name: "@quest.odd_jobs.name"
819
- description: "@quest.odd_jobs.description"
820
- stages:
821
- - id: started
822
- description: "@quest.odd_jobs.stage.started"
823
- - id: talked_to_merchant
824
- description: "@quest.odd_jobs.stage.talked_to_merchant"
825
- - id: complete
826
- description: "@quest.odd_jobs.stage.complete"
827
- `), await m(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
828
- title: "@journal.tavern_discovery.title"
829
- text: "@journal.tavern_discovery.text"
830
- category: places
831
- `), await m(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
832
- title: "@journal.odd_jobs_accepted.title"
833
- text: "@journal.odd_jobs_accepted.text"
834
- category: quests
835
- `), await m(s(e, "content/journal/market_square.yaml"), `id: market_square
836
- title: "@journal.market_square.title"
837
- text: "@journal.market_square.text"
838
- category: places
839
- `), await m(s(e, "content/interludes/chapter_one.yaml"), `id: chapter_one
840
- # Background image shown fullscreen during the interlude.
841
- # Replace with your own image in assets/images/.
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. Supports @localization.key or plain text.
851
- text: |
852
- Chapter One: A New Beginning
853
-
854
- The road behind you stretches long and empty.
855
- Ahead, the lights of town flicker through the evening mist.
856
-
857
- You have heard the rumours. Strange things happening.
858
- People going missing. Shadows that move wrong.
859
-
860
- Someone has to look into it.
861
-
862
- It might as well be you.
863
-
864
- # Auto-trigger when the player enters the tavern for the first time.
865
- # The seenChapterOne flag is set in content/dialogues/tavern_intro.dlg.
866
- triggerLocation: tavern
867
- triggerConditions:
868
- - type: notFlag
869
- flag: seenChapterOne
870
- `), await m(s(e, "content/dialogues/tavern_intro.dlg"), `# This dialogue triggers automatically when the player enters the tavern.
871
- # TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
872
- # Use notFlag to make it a one-time intro.
873
-
874
- TRIGGER tavern
875
- REQUIRE notFlag seenTavernIntro
876
-
877
- # Each NODE is a conversation point. The first NODE is always the start.
878
- NODE start
879
- # NARRATOR: has no speaker — used for scene-setting text.
880
- # @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
881
- # You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
882
- NARRATOR: @narrator.tavern_intro
883
-
884
- # Effects run immediately when this node is reached, before choices are shown.
885
- SET flag seenTavernIntro
886
- # Mark the chapter interlude as seen so it doesn't replay on return visits.
887
- SET flag seenChapterOne
888
-
889
- # CHOICE text can use a @key or "inline text".
890
- # A choice with END dialogue is a terminal choice — no GOTO needed.
891
- CHOICE @narrator.choice.look_around
892
- END dialogue
893
- END
894
- `), await m(s(e, "content/dialogues/market_intro.dlg"), `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
895
-
896
- TRIGGER market
897
- REQUIRE notFlag seenMarketIntro
898
-
899
- NODE start
900
- NARRATOR: @narrator.market_intro
901
- SET flag seenMarketIntro
902
-
903
- # ADD journalEntry unlocks a journal entry for the player.
904
- ADD journalEntry market_square
905
-
906
- CHOICE @narrator.choice.look_around
907
- END dialogue
908
- END
909
- `), await m(s(e, "content/dialogues/bartender_greeting.dlg"), `# This dialogue is triggered by clicking the bartender character.
910
- # SPEAKER: lines set who's talking — matched to character ID (case-insensitive).
911
- # Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
574
+ `, ce = `# This dialogue is triggered by clicking the bartender character.
575
+ # SPEAKER: lines set who's talking — matched to character ID (case-insensitive).
576
+ # Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
912
577
 
913
578
  NODE start
914
579
  BARTENDER: @bartender.greeting
@@ -1048,7 +713,65 @@ NODE work_done
1048
713
  NODE farewell
1049
714
  BARTENDER: @bartender.farewell
1050
715
  END dialogue
1051
- `), await m(s(e, "content/dialogues/merchant_intro.dlg"), `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
716
+ `, le = `# Skill check example — demonstrates dice rolling and variable interpolation.
717
+ #
718
+ # ROLL <variable> <min> <max> — rolls a random integer and stores it in a variable.
719
+ # {varName} — in dialogue text, replaced with the variable's value.
720
+ # roll <min> <max> <threshold> — condition: rolls and returns true if result >= threshold.
721
+ #
722
+ # This dialogue is NOT auto-triggered — start it from another node with:
723
+ # START dialogue bluff_check
724
+
725
+ NODE start
726
+ # Roll a d20 and store it as "bluffRoll" before the player sees the result
727
+ ROLL bluffRoll 1 20
728
+ NARRATOR: @bluff.setup
729
+
730
+ CHOICE @bluff.choice.attempt
731
+ GOTO resolve
732
+ END
733
+
734
+ CHOICE @bluff.choice.back_down
735
+ NARRATOR: @bluff.backed_down
736
+ END dialogue
737
+ END
738
+
739
+ NODE resolve
740
+ # Show the roll result using {bluffRoll} interpolation in the locale string
741
+ NARRATOR: @bluff.rolled
742
+
743
+ # Branch on whether the stored variable passes a threshold (15+)
744
+ IF variableGreaterThan bluffRoll 14
745
+ GOTO success
746
+ END
747
+
748
+ GOTO failure
749
+
750
+ NODE success
751
+ NARRATOR: @bluff.success
752
+ ADD relationship bartender 2
753
+ SET flag bluffedMarcus
754
+ END dialogue
755
+
756
+ NODE failure
757
+ NARRATOR: @bluff.failure
758
+ END dialogue
759
+ `, de = `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
760
+
761
+ TRIGGER market
762
+ REQUIRE notFlag seenMarketIntro
763
+
764
+ NODE start
765
+ NARRATOR: @narrator.market_intro
766
+ SET flag seenMarketIntro
767
+
768
+ # ADD journalEntry unlocks a journal entry for the player.
769
+ ADD journalEntry market_square
770
+
771
+ CHOICE @narrator.choice.look_around
772
+ END dialogue
773
+ END
774
+ `, ue = `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
1052
775
  # The quest choices here demonstrate multi-stage quest gating with questAtStage.
1053
776
 
1054
777
  NODE start
@@ -1151,7 +874,94 @@ NODE about_market
1151
874
  NODE farewell
1152
875
  MERCHANT: @merchant.farewell
1153
876
  END dialogue
1154
- `), await m(s(e, "content/locales/en.yaml"), `# ===================
877
+ `, he = `# This dialogue triggers automatically when the player enters the tavern.
878
+ # TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
879
+ # Use notFlag to make it a one-time intro.
880
+
881
+ TRIGGER tavern
882
+ REQUIRE notFlag seenTavernIntro
883
+
884
+ # Each NODE is a conversation point. The first NODE is always the start.
885
+ NODE start
886
+ # NARRATOR: has no speaker — used for scene-setting text.
887
+ # @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
888
+ # You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
889
+ NARRATOR: @narrator.tavern_intro
890
+
891
+ # Effects run immediately when this node is reached, before choices are shown.
892
+ SET flag seenTavernIntro
893
+
894
+ # CHOICE text can use a @key or "inline text".
895
+ # A choice with END dialogue is a terminal choice — no GOTO needed.
896
+ CHOICE @narrator.choice.look_around
897
+ END dialogue
898
+ END
899
+ `, me = `# Game Configuration
900
+ startLocation: tavern
901
+ startTime:
902
+ day: 1
903
+ hour: 8
904
+ startFlags: {}
905
+ startVariables:
906
+ gold: 100
907
+ reputation: 0
908
+ _drinksBought: 0
909
+ startInventory: []
910
+ `, ge = `id: chapter_one
911
+ # Background image shown fullscreen during the interlude.
912
+ # Replace with your own image in assets/images/.
913
+ background: /assets/images/banners/tavern_banner.jpg
914
+
915
+ # Optional: decorative border/frame image overlaid on the background.
916
+ # banner: /assets/images/ui/chapter_frame.png
917
+
918
+ # Optional: music to play during this interlude.
919
+ # music: /assets/audio/music/chapter_theme.ogg
920
+
921
+ # The narrative text. Supports @localization.key or plain text.
922
+ text: |
923
+ Chapter One: A New Beginning
924
+
925
+ The road behind you stretches long and empty.
926
+ Ahead, the lights of town flicker through the evening mist.
927
+
928
+ You have heard the rumours. Strange things happening.
929
+ People going missing. Shadows that move wrong.
930
+
931
+ Someone has to look into it.
932
+
933
+ It might as well be you.
934
+
935
+ # Auto-trigger when the player enters the tavern for the first time.
936
+ # triggerConditions prevents re-triggering. effects runs when the interlude fires
937
+ # — setFlag here marks it seen so it won't show again on return visits.
938
+ triggerLocation: tavern
939
+ triggerConditions:
940
+ - type: notFlag
941
+ flag: seenChapterOne
942
+ effects:
943
+ - type: setFlag
944
+ flag: seenChapterOne
945
+ `, fe = `id: old_coin
946
+ name: "@item.old_coin.name"
947
+ description: "@item.old_coin.description"
948
+ icon: ""
949
+ image: ""
950
+ location: tavern
951
+ stats: {}
952
+ `, pe = `id: market_square
953
+ title: "@journal.market_square.title"
954
+ text: "@journal.market_square.text"
955
+ category: places
956
+ `, ye = `id: odd_jobs_accepted
957
+ title: "@journal.odd_jobs_accepted.title"
958
+ text: "@journal.odd_jobs_accepted.text"
959
+ category: quests
960
+ `, _e = `id: tavern_discovery
961
+ title: "@journal.tavern_discovery.title"
962
+ text: "@journal.tavern_discovery.text"
963
+ category: places
964
+ `, be = `# ===================
1155
965
  # Narrator Intros
1156
966
  # ===================
1157
967
  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."
@@ -1279,24 +1089,307 @@ notification.quest_updated: "Quest Updated: Odd Jobs"
1279
1089
  notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
1280
1090
  notification.bought_drink: "Bought a drink (-5 gold)"
1281
1091
  notification.bought_map: "Bought a map (-20 gold)"
1282
- `), 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
1283
- dist
1284
- .DS_Store
1285
- *.log
1286
- `);
1092
+
1093
+ # ===================
1094
+ # Skill Check (bluff_check.dlg)
1095
+ # ===================
1096
+ bluff.setup: "Marcus eyes you across the bar. You consider spinning him a tale to get a discount on that drink..."
1097
+ bluff.choice.attempt: "Try to bluff him."
1098
+ bluff.choice.back_down: "Actually, never mind."
1099
+ bluff.backed_down: "Some fights aren't worth picking."
1100
+ bluff.rolled: "You spin the tale with {bluffRoll} on your roll — and Marcus listens carefully."
1101
+ bluff.success: "The story lands perfectly. Marcus laughs and slides a free drink across the bar. Not bad at all, he admits."
1102
+ bluff.failure: "Marcus raises an eyebrow, entirely unconvinced. That will be five gold, he says flatly."
1103
+ `, we = `id: market
1104
+ name: "@location.market.name"
1105
+ description: "@location.market.description"
1106
+ banner: ""
1107
+ music: ""
1108
+ ambient: ""
1109
+ `, ke = `id: tavern
1110
+ name: "@location.tavern.name"
1111
+ description: "@location.tavern.description"
1112
+ banner: ""
1113
+ music: ""
1114
+ ambient: ""
1115
+ `, Ee = `id: town
1116
+ name: "@map.town.name"
1117
+ image: ""
1118
+ scale: 1
1119
+ locations:
1120
+ - id: tavern
1121
+ x: 100
1122
+ y: 200
1123
+ - id: market
1124
+ x: 300
1125
+ y: 150
1126
+ `, ve = `id: odd_jobs
1127
+ name: "@quest.odd_jobs.name"
1128
+ description: "@quest.odd_jobs.description"
1129
+ stages:
1130
+ - id: started
1131
+ description: "@quest.odd_jobs.stage.started"
1132
+ - id: talked_to_merchant
1133
+ description: "@quest.odd_jobs.stage.talked_to_merchant"
1134
+ - id: complete
1135
+ description: "@quest.odd_jobs.stage.complete"
1136
+ `, Oe = `import { useEffect, useState } from 'react'
1137
+ import { Engine } from '@doodle-engine/core'
1138
+ import type { GameState, Snapshot } from '@doodle-engine/core'
1139
+ import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
1140
+
1141
+ export function App() {
1142
+ const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
1143
+
1144
+ useEffect(() => {
1145
+ fetch('/api/content')
1146
+ .then(res => res.json())
1147
+ .then(data => {
1148
+ const engine = new Engine(data.registry, createEmptyState())
1149
+ const snapshot = engine.newGame(data.config)
1150
+ setGame({ engine, snapshot })
1151
+ })
1152
+ }, [])
1153
+
1154
+ if (!game) {
1155
+ return <LoadingScreen />
1156
+ }
1157
+
1158
+ return (
1159
+ <GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
1160
+ <GameUI />
1161
+ </GameProvider>
1162
+ )
1163
+ }
1164
+
1165
+ function GameUI() {
1166
+ const { snapshot, actions } = useGame()
1167
+
1168
+ return (
1169
+ <div style={{ padding: '2rem', fontFamily: 'sans-serif', maxWidth: '800px', margin: '0 auto' }}>
1170
+ <h1>{snapshot.location.name}</h1>
1171
+ <p>{snapshot.location.description}</p>
1172
+
1173
+ {snapshot.dialogue && (
1174
+ <div style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px', margin: '1rem 0' }}>
1175
+ <strong>{snapshot.dialogue.speakerName}:</strong>
1176
+ <p>{snapshot.dialogue.text}</p>
1177
+ {snapshot.choices.map(choice => (
1178
+ <button
1179
+ key={choice.id}
1180
+ onClick={() => actions.selectChoice(choice.id)}
1181
+ style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
1182
+ >
1183
+ {choice.text}
1184
+ </button>
1185
+ ))}
1186
+ </div>
1187
+ )}
1188
+
1189
+ {!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
1190
+ <div>
1191
+ <h2>Characters here</h2>
1192
+ {snapshot.charactersHere.map(char => (
1193
+ <button
1194
+ key={char.id}
1195
+ onClick={() => actions.talkTo(char.id)}
1196
+ style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
1197
+ >
1198
+ Talk to {char.name}
1199
+ </button>
1200
+ ))}
1201
+ </div>
1202
+ )}
1203
+ </div>
1204
+ )
1205
+ }
1206
+
1207
+ function createEmptyState(): GameState {
1208
+ return {
1209
+ currentLocation: '',
1210
+ currentTime: { day: 1, hour: 0 },
1211
+ flags: {},
1212
+ variables: {},
1213
+ inventory: [],
1214
+ questProgress: {},
1215
+ unlockedJournalEntries: [],
1216
+ playerNotes: [],
1217
+ dialogueState: null,
1218
+ characterState: {},
1219
+ itemLocations: {},
1220
+ mapEnabled: true,
1221
+ notifications: [],
1222
+ pendingSounds: [],
1223
+ pendingVideo: null,
1224
+ pendingInterlude: null,
1225
+ currentLocale: 'en',
1226
+ }
1287
1227
  }
1288
- const k = new G();
1289
- k.name("doodle").description(c.magenta("ðŸū Doodle Engine") + c.dim(" — Narrative RPG development tools")).version("0.0.1");
1290
- k.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
1291
- await ne(e);
1228
+ `, De = `import { useEffect, useState } from 'react'
1229
+ import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
1230
+ import { GameShell, LoadingScreen } from '@doodle-engine/react'
1231
+
1232
+ export function App() {
1233
+ const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
1234
+
1235
+ useEffect(() => {
1236
+ fetch('/api/content')
1237
+ .then(res => res.json())
1238
+ .then(data => setContent({ registry: data.registry, config: data.config }))
1239
+ }, [])
1240
+
1241
+ if (!content) {
1242
+ return <LoadingScreen />
1243
+ }
1244
+
1245
+ return (
1246
+ <GameShell
1247
+ registry={content.registry}
1248
+ config={content.config}
1249
+ title="My Doodle Game"
1250
+ subtitle="A text-based adventure"
1251
+ splashDuration={2000}
1252
+ availableLocales={[{ code: 'en', label: 'English' }]}
1253
+ devTools={import.meta.env.DEV}
1254
+ />
1255
+ )
1256
+ }
1257
+ `, Te = `body {
1258
+ margin: 0;
1259
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
1260
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
1261
+ sans-serif;
1262
+ -webkit-font-smoothing: antialiased;
1263
+ -moz-osx-font-smoothing: grayscale;
1264
+ }
1265
+ `, Ie = `import { StrictMode } from 'react'
1266
+ import { createRoot } from 'react-dom/client'
1267
+ import { App } from './App'
1268
+ import './index.css'
1269
+
1270
+ createRoot(document.getElementById('root')!).render(
1271
+ <StrictMode>
1272
+ <App />
1273
+ </StrictMode>,
1274
+ )
1275
+ `, O = "ðŸū", Ne = "🐕", x = "ðŸĶī", Ce = "âœĻ", q = "📁", T = "✅", Re = "🚀", S = /* @__PURE__ */ Object.assign({
1276
+ "./templates/_root/_gitignore": oe,
1277
+ "./templates/_root/index.html": ae,
1278
+ "./templates/_root/tsconfig.json": re,
1279
+ "./templates/content/characters/bartender.yaml": se,
1280
+ "./templates/content/characters/merchant.yaml": ie,
1281
+ "./templates/content/dialogues/bartender_greeting.dlg": ce,
1282
+ "./templates/content/dialogues/bluff_check.dlg": le,
1283
+ "./templates/content/dialogues/market_intro.dlg": de,
1284
+ "./templates/content/dialogues/merchant_intro.dlg": ue,
1285
+ "./templates/content/dialogues/tavern_intro.dlg": he,
1286
+ "./templates/content/game.yaml": me,
1287
+ "./templates/content/interludes/chapter_one.yaml": ge,
1288
+ "./templates/content/items/old_coin.yaml": fe,
1289
+ "./templates/content/journal/market_square.yaml": pe,
1290
+ "./templates/content/journal/odd_jobs_accepted.yaml": ye,
1291
+ "./templates/content/journal/tavern_discovery.yaml": _e,
1292
+ "./templates/content/locales/en.yaml": be,
1293
+ "./templates/content/locations/market.yaml": we,
1294
+ "./templates/content/locations/tavern.yaml": ke,
1295
+ "./templates/content/maps/town.yaml": Ee,
1296
+ "./templates/content/quests/odd_jobs.yaml": ve,
1297
+ "./templates/src/App.custom.tsx": Oe,
1298
+ "./templates/src/App.default.tsx": De,
1299
+ "./templates/src/index.css": Te,
1300
+ "./templates/src/main.tsx": Ie
1292
1301
  });
1293
- k.command("dev").description("Start development server with hot reload").action(async () => {
1294
- await Y();
1302
+ function Ae(n) {
1303
+ const o = n.replace("./templates/", "");
1304
+ if (o === "src/App.default.tsx" || o === "src/App.custom.tsx") return null;
1305
+ if (o.startsWith("_root/")) {
1306
+ const r = o.slice(6);
1307
+ return r.startsWith("_") ? "." + r.slice(1) : r;
1308
+ }
1309
+ return o;
1310
+ }
1311
+ async function Se(n) {
1312
+ const o = d(process.cwd(), n);
1313
+ 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(` ${Ne} Creating new game: ${i.bold.cyan(n)}`), console.log("");
1314
+ const { useDefaultRenderer: r } = await P({
1315
+ type: "confirm",
1316
+ name: "useDefaultRenderer",
1317
+ message: "Use default renderer?",
1318
+ initial: !0
1319
+ });
1320
+ r === void 0 && (console.log(i.yellow(`
1321
+ ${x} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await je(o, n, r), console.log(""), console.log(i.bold.green(` ${T} Project created successfully!`)), console.log(""), console.log(i.dim(` ${q} ${o}`)), console.log(""), console.log(i.bold(" Next steps:")), console.log(i.cyan(` cd ${n}`)), 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(` ${Re} Happy game making! ${O}`)), console.log("");
1322
+ }
1323
+ async function je(n, o, r) {
1324
+ const a = [
1325
+ "content/locations",
1326
+ "content/characters",
1327
+ "content/items",
1328
+ "content/dialogues",
1329
+ "content/quests",
1330
+ "content/journal",
1331
+ "content/interludes",
1332
+ "content/locales",
1333
+ "content/maps",
1334
+ "assets/images/banners",
1335
+ "assets/images/portraits",
1336
+ "assets/images/items",
1337
+ "assets/images/maps",
1338
+ "assets/audio/music",
1339
+ "assets/audio/sfx",
1340
+ "assets/audio/voice",
1341
+ "src"
1342
+ ];
1343
+ console.log(` ${q} ${i.bold("Creating directories...")}`);
1344
+ for (const e of a)
1345
+ await D(d(n, e), { recursive: !0 });
1346
+ console.log(i.green(` ${T} Directories created`)), console.log("");
1347
+ const t = {
1348
+ name: o,
1349
+ version: "0.1.0",
1350
+ type: "module",
1351
+ scripts: {
1352
+ dev: "doodle dev",
1353
+ build: "doodle build",
1354
+ preview: "vite preview"
1355
+ },
1356
+ dependencies: {
1357
+ "@doodle-engine/core": "latest",
1358
+ "@doodle-engine/react": "latest",
1359
+ react: "^19.0.0",
1360
+ "react-dom": "^19.0.0"
1361
+ },
1362
+ devDependencies: {
1363
+ "@doodle-engine/cli": "latest",
1364
+ "@types/react": "^19.0.0",
1365
+ "@types/react-dom": "^19.0.0",
1366
+ "@vitejs/plugin-react": "^4.3.0",
1367
+ typescript: "^5.7.0",
1368
+ vite: "^6.0.0"
1369
+ }
1370
+ };
1371
+ console.log(` ${Ce} ${i.bold("Writing project files...")}`), await k(d(n, "package.json"), JSON.stringify(t, null, 2));
1372
+ for (const [e, c] of Object.entries(S)) {
1373
+ const l = Ae(e);
1374
+ if (l === null) continue;
1375
+ const u = d(n, l);
1376
+ await D(F(u), { recursive: !0 }), await k(u, c);
1377
+ }
1378
+ const s = r ? "./templates/src/App.default.tsx" : "./templates/src/App.custom.tsx";
1379
+ await k(d(n, "src/App.tsx"), S[s]), console.log(i.green(` ${T} Source files created`)), console.log(""), console.log(` ${x} ${i.bold("Starter content written")}`), 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 with 2 locations)")), console.log(i.dim(" 1 quest (odd jobs, 3 stages)")), console.log(i.dim(" 3 journal entries")), console.log(i.dim(" 1 interlude (chapter one, auto-triggers at tavern)")), console.log(i.dim(" 5 dialogues (2 narrator intros, 2 NPC conversations, 1 skill check)")), console.log(i.dim(" English locale with all strings"));
1380
+ }
1381
+ const b = new G();
1382
+ b.name("doodle").description(i.magenta("ðŸū Doodle Engine") + i.dim(" — Narrative RPG development tools")).version("0.0.1");
1383
+ b.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (n) => {
1384
+ await Se(n);
1295
1385
  });
1296
- k.command("build").description("Build game for production").action(async () => {
1297
- await z();
1386
+ b.command("dev").description("Start development server with hot reload").action(async () => {
1387
+ await K();
1298
1388
  });
1299
- k.command("validate").description("Validate game content").action(async () => {
1389
+ b.command("build").description("Build game for production").action(async () => {
1300
1390
  await Z();
1301
1391
  });
1302
- k.parse();
1392
+ b.command("validate").description("Validate game content").action(async () => {
1393
+ await ne();
1394
+ });
1395
+ b.parse();