@doodle-engine/cli 0.0.12 → 0.0.14

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