@doodle-engine/cli 0.0.21 → 0.0.23

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
@@ -9,73 +9,73 @@ import { join as h, extname as f, relative as E, dirname as U } from "path";
9
9
  import { parse as y } from "yaml";
10
10
  import { extractAssetPaths as W, getAssetType as R, parseDialogue as k } from "@doodle-engine/core";
11
11
  import J from "prompts";
12
- function C(n, o) {
12
+ function C(t, o) {
13
13
  const s = [];
14
- for (const r of Object.values(n.dialogues)) {
15
- const t = o.get(r.id) || `dialogue:${r.id}`;
16
- s.push(...Q(r, t));
14
+ for (const r of Object.values(t.dialogues)) {
15
+ const n = o.get(r.id) || `dialogue:${r.id}`;
16
+ s.push(...Q(r, n));
17
17
  }
18
- for (const r of Object.values(n.characters))
19
- if (r.dialogue && !n.dialogues[r.dialogue]) {
20
- const t = o.get(r.id) || `character:${r.id}`;
18
+ for (const r of Object.values(t.characters))
19
+ if (r.dialogue && !t.dialogues[r.dialogue]) {
20
+ const n = o.get(r.id) || `character:${r.id}`;
21
21
  s.push({
22
- file: t,
22
+ file: n,
23
23
  message: `Character "${r.id}" references non-existent dialogue "${r.dialogue}"`,
24
24
  suggestion: `Create dialogue "${r.dialogue}" or fix the reference`
25
25
  });
26
26
  }
27
- return s.push(...K(n, o)), s;
27
+ return s.push(...K(t, o)), s;
28
28
  }
29
- function Q(n, o) {
29
+ function Q(t, o) {
30
30
  const s = [], r = /* @__PURE__ */ new Set();
31
- for (const t of n.nodes)
32
- r.has(t.id) && s.push({
31
+ for (const n of t.nodes)
32
+ r.has(n.id) && s.push({
33
33
  file: o,
34
- message: `Duplicate node ID "${t.id}"`,
34
+ message: `Duplicate node ID "${n.id}"`,
35
35
  suggestion: "Node IDs must be unique within a dialogue"
36
- }), r.add(t.id);
37
- r.has(n.startNode) || s.push({
36
+ }), r.add(n.id);
37
+ r.has(t.startNode) || s.push({
38
38
  file: o,
39
- message: `Start node "${n.startNode}" not found`,
40
- suggestion: `Add a NODE ${n.startNode} or fix the startNode reference`
39
+ message: `Start node "${t.startNode}" not found`,
40
+ suggestion: `Add a NODE ${t.startNode} or fix the startNode reference`
41
41
  });
42
- for (const t of n.nodes)
43
- s.push(...Y(t, r, o));
42
+ for (const n of t.nodes)
43
+ s.push(...Y(n, r, o));
44
44
  return s;
45
45
  }
46
- function Y(n, o, s) {
46
+ function Y(t, o, s) {
47
47
  const r = [];
48
- if (n.next && !o.has(n.next) && r.push({
48
+ if (t.next && !o.has(t.next) && r.push({
49
49
  file: s,
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) || r.push({
50
+ message: `Node "${t.id}" GOTO "${t.next}" points to non-existent node`,
51
+ suggestion: `Add NODE ${t.next} or fix the GOTO target`
52
+ }), t.conditionalNext)
53
+ for (const n of t.conditionalNext)
54
+ o.has(n.next) || r.push({
55
55
  file: s,
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
- }), r.push(...O(t.condition, n.id, s));
59
- for (const t of n.choices) {
60
- if (!t.effects?.some(
56
+ message: `Node "${t.id}" IF block GOTO "${n.next}" points to non-existent node`,
57
+ suggestion: `Add NODE ${n.next} or fix the GOTO target`
58
+ }), r.push(...O(n.condition, t.id, s));
59
+ for (const n of t.choices) {
60
+ if (!n.effects?.some(
61
61
  (e) => e.type === "endDialogue" || e.type === "goToLocation" || e.type === "startDialogue"
62
- ) && t.next && !o.has(t.next) && r.push({
62
+ ) && n.next && !o.has(n.next) && r.push({
63
63
  file: s,
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
- r.push(...O(e, n.id, s));
69
- if (t.effects)
70
- for (const e of t.effects)
71
- r.push(...A(e, n.id, s));
64
+ message: `Node "${t.id}" choice "${n.id}" GOTO "${n.next}" points to non-existent node`,
65
+ suggestion: `Add NODE ${n.next} or fix the GOTO target`
66
+ }), n.conditions)
67
+ for (const e of n.conditions)
68
+ r.push(...O(e, t.id, s));
69
+ if (n.effects)
70
+ for (const e of n.effects)
71
+ r.push(...A(e, t.id, s));
72
72
  }
73
- if (n.conditions)
74
- for (const t of n.conditions)
75
- r.push(...O(t, n.id, s));
76
- if (n.effects)
77
- for (const t of n.effects)
78
- r.push(...A(t, n.id, s));
73
+ if (t.conditions)
74
+ for (const n of t.conditions)
75
+ r.push(...O(n, t.id, s));
76
+ if (t.effects)
77
+ for (const n of t.effects)
78
+ r.push(...A(n, t.id, s));
79
79
  return r;
80
80
  }
81
81
  const z = {
@@ -123,51 +123,51 @@ const z = {
123
123
  showInterlude: ["interludeId"],
124
124
  roll: ["variable", "min", "max"]
125
125
  };
126
- function O(n, o, s) {
126
+ function O(t, o, s) {
127
127
  const r = [];
128
- if (!n.type)
128
+ if (!t.type)
129
129
  return r.push({
130
130
  file: s,
131
131
  message: `Node "${o}" has condition with missing type`
132
132
  }), r;
133
- if (n.type === "timeIs")
134
- return n.hour === void 0 && n.day === void 0 && r.push({
133
+ if (t.type === "timeIs")
134
+ return t.hour === void 0 && t.day === void 0 && r.push({
135
135
  file: s,
136
136
  message: `Node "${o}" condition "timeIs" must have at least one of "hour" or "day" argument`
137
137
  }), r;
138
- const t = z[n.type];
139
- if (!t)
138
+ const n = z[t.type];
139
+ if (!n)
140
140
  return r;
141
- for (const a of t)
142
- (n[a] === void 0 || n[a] === null || n[a] === "") && r.push({
141
+ for (const a of n)
142
+ (t[a] === void 0 || t[a] === null || t[a] === "") && r.push({
143
143
  file: s,
144
- message: `Node "${o}" condition "${n.type}" missing required "${a}" argument`
144
+ message: `Node "${o}" condition "${t.type}" missing required "${a}" argument`
145
145
  });
146
146
  return r;
147
147
  }
148
- function A(n, o, s) {
148
+ function A(t, o, s) {
149
149
  const r = [];
150
- if (!n.type)
150
+ if (!t.type)
151
151
  return r.push({
152
152
  file: s,
153
153
  message: `Node "${o}" has effect with missing type`
154
154
  }), r;
155
- const t = V[n.type];
156
- if (!t)
155
+ const n = V[t.type];
156
+ if (!n)
157
157
  return r;
158
- for (const a of t)
159
- (n[a] === void 0 || n[a] === null || n[a] === "") && r.push({
158
+ for (const a of n)
159
+ (t[a] === void 0 || t[a] === null || t[a] === "") && r.push({
160
160
  file: s,
161
- message: `Node "${o}" effect "${n.type}" missing required "${a}" argument`
161
+ message: `Node "${o}" effect "${t.type}" missing required "${a}" argument`
162
162
  });
163
163
  return r;
164
164
  }
165
- function K(n, o) {
165
+ function K(t, o) {
166
166
  const s = [], r = /* @__PURE__ */ new Set();
167
- for (const e of Object.values(n.locales))
167
+ for (const e of Object.values(t.locales))
168
168
  for (const i of Object.keys(e))
169
169
  r.add(i);
170
- const t = (e) => e.startsWith("@"), a = (e, i, l) => {
170
+ const n = (e) => e.startsWith("@"), a = (e, i, l) => {
171
171
  const d = e.slice(1);
172
172
  if (!r.has(d)) {
173
173
  const u = o.get(i) || `${l}:${i}`;
@@ -178,45 +178,47 @@ function K(n, o) {
178
178
  });
179
179
  }
180
180
  };
181
- for (const e of Object.values(n.locations))
182
- t(e.name) && a(e.name, e.id, "location"), t(e.description) && a(e.description, e.id, "location");
183
- for (const e of Object.values(n.characters))
184
- t(e.name) && a(e.name, e.id, "character"), t(e.biography) && a(e.biography, e.id, "character");
185
- for (const e of Object.values(n.items))
186
- t(e.name) && a(e.name, e.id, "item"), t(e.description) && a(e.description, e.id, "item");
187
- for (const e of Object.values(n.quests)) {
188
- t(e.name) && a(e.name, e.id, "quest"), t(e.description) && a(e.description, e.id, "quest");
181
+ for (const e of Object.values(t.locations))
182
+ n(e.name) && a(e.name, e.id, "location"), n(e.description) && a(e.description, e.id, "location");
183
+ for (const e of Object.values(t.characters))
184
+ n(e.name) && a(e.name, e.id, "character"), n(e.biography) && a(e.biography, e.id, "character");
185
+ for (const e of Object.values(t.items))
186
+ n(e.name) && a(e.name, e.id, "item"), n(e.description) && a(e.description, e.id, "item");
187
+ for (const e of Object.values(t.quests)) {
188
+ n(e.name) && a(e.name, e.id, "quest"), n(e.description) && a(e.description, e.id, "quest");
189
189
  for (const i of e.stages)
190
- t(i.description) && a(i.description, e.id, "quest");
190
+ n(i.description) && a(i.description, e.id, "quest");
191
191
  }
192
- for (const e of Object.values(n.journalEntries))
193
- t(e.title) && a(e.title, e.id, "journal"), t(e.text) && a(e.text, e.id, "journal");
194
- for (const e of Object.values(n.dialogues))
192
+ for (const e of Object.values(t.journalEntries))
193
+ n(e.title) && a(e.title, e.id, "journal"), n(e.text) && a(e.text, e.id, "journal");
194
+ for (const e of Object.values(t.dialogues))
195
195
  for (const i of e.nodes) {
196
- t(i.text) && a(i.text, e.id, "dialogue");
196
+ n(i.text) && a(i.text, e.id, "dialogue");
197
197
  for (const l of i.choices)
198
- t(l.text) && a(l.text, e.id, "dialogue");
198
+ n(l.text) && a(l.text, e.id, "dialogue");
199
199
  }
200
- for (const e of Object.values(n.interludes))
201
- t(e.text) && a(e.text, e.id, "interlude");
200
+ for (const e of Object.values(t.interludes))
201
+ n(e.text) && a(e.text, e.id, "interlude");
202
202
  return s;
203
203
  }
204
- function I(n) {
205
- if (n.length === 0) {
204
+ function I(t) {
205
+ if (t.length === 0) {
206
206
  console.log(c.green("✓ No validation errors"));
207
207
  return;
208
208
  }
209
209
  console.log(
210
210
  c.red(
211
211
  `
212
- ✗ Found ${n.length} validation error${n.length === 1 ? "" : "s"}:
212
+ ✗ Found ${t.length} validation error${t.length === 1 ? "" : "s"}:
213
213
  `
214
214
  )
215
215
  );
216
- for (const o of n)
217
- console.log(c.bold(o.file) + (o.line ? `:${o.line}` : "")), console.log(" " + c.red(o.message)), o.suggestion && console.log(" " + c.dim(o.suggestion)), console.log();
216
+ for (const o of t)
217
+ console.log(
218
+ c.bold(o.file) + (o.line ? `:${o.line}` : "")
219
+ ), console.log(" " + c.red(o.message)), o.suggestion && console.log(" " + c.dim(o.suggestion)), console.log();
218
220
  }
219
- async function q(n, o, s, r, t = Date.now().toString()) {
221
+ async function q(t, o, s, r, n = Date.now().toString()) {
220
222
  const { shell: a, game: e } = W(
221
223
  s,
222
224
  r
@@ -252,7 +254,7 @@ async function q(n, o, s, r, t = Date.now().toString()) {
252
254
  )
253
255
  ), u = l.reduce((g, _) => g + (_.size ?? 0), 0), m = d.reduce((g, _) => g + (_.size ?? 0), 0);
254
256
  return {
255
- version: t,
257
+ version: n,
256
258
  shell: l,
257
259
  game: d,
258
260
  shellSize: u,
@@ -260,31 +262,43 @@ async function q(n, o, s, r, t = Date.now().toString()) {
260
262
  };
261
263
  }
262
264
  const S = "🐾", X = "✨", Z = "✏️", ee = "➕";
263
- async function ne() {
264
- const n = process.cwd(), o = h(n, "content");
265
- console.log(""), console.log(c.bold.magenta(` ${S} Doodle Engine Dev Server ${S}`)), console.log("");
265
+ async function te() {
266
+ const t = process.cwd(), o = h(t, "content");
267
+ console.log(""), console.log(
268
+ c.bold.magenta(` ${S} Doodle Engine Dev Server ${S}`)
269
+ ), console.log("");
266
270
  const s = {
267
271
  name: "doodle-content-loader",
268
- configureServer(t) {
269
- t.middlewares.use("/api/content", async (d, u) => {
272
+ configureServer(n) {
273
+ n.middlewares.use("/api/content", async (d, u) => {
270
274
  try {
271
275
  const m = await j(o);
272
276
  u.setHeader("Content-Type", "application/json"), u.end(JSON.stringify(m));
273
277
  } catch (m) {
274
- console.error(c.red(" Error loading content:"), m), u.statusCode = 500, u.end(JSON.stringify({ error: "Failed to load content" }));
278
+ console.error(
279
+ c.red(" Error loading content:"),
280
+ m
281
+ ), u.statusCode = 500, u.end(
282
+ JSON.stringify({ error: "Failed to load content" })
283
+ );
275
284
  }
276
- }), t.middlewares.use("/api/manifest", async (d, u) => {
285
+ }), n.middlewares.use("/api/manifest", async (d, u) => {
277
286
  try {
278
287
  const { registry: m, config: g } = await j(o), _ = await q(
279
- h(n, "assets"),
280
- n,
288
+ h(t, "assets"),
289
+ t,
281
290
  m,
282
291
  g,
283
292
  "dev"
284
293
  );
285
294
  u.setHeader("Content-Type", "application/json"), u.end(JSON.stringify(_));
286
295
  } catch (m) {
287
- console.error(c.red(" Error generating manifest:"), m), u.statusCode = 500, u.end(JSON.stringify({ error: "Failed to generate manifest" }));
296
+ console.error(
297
+ c.red(" Error generating manifest:"),
298
+ m
299
+ ), u.statusCode = 500, u.end(
300
+ JSON.stringify({ error: "Failed to generate manifest" })
301
+ );
288
302
  }
289
303
  });
290
304
  const a = L(o, {
@@ -298,26 +312,32 @@ async function ne() {
298
312
  let i = null;
299
313
  const l = (d) => {
300
314
  i && clearTimeout(i), i = setTimeout(async () => {
301
- i = null, console.log(d), await te(o), t.ws.send({ type: "full-reload", path: "*" });
315
+ i = null, console.log(d), await ne(o), n.ws.send({ type: "full-reload", path: "*" });
302
316
  }, 50);
303
317
  };
304
318
  a.on("change", (d) => {
305
- l(c.yellow(` ${Z} Content changed: ${d}`));
319
+ l(
320
+ c.yellow(` ${Z} Content changed: ${d}`)
321
+ );
306
322
  }), a.on("add", (d) => {
307
- e && l(c.green(` ${ee} Content added: ${d}`));
323
+ e && l(
324
+ c.green(` ${ee} Content added: ${d}`)
325
+ );
308
326
  });
309
327
  }
310
328
  }, r = await F({
311
- root: n,
329
+ root: t,
312
330
  plugins: [G(), s],
313
331
  server: {
314
332
  port: 3e3,
315
333
  open: !0
316
334
  }
317
335
  });
318
- await r.listen(), r.printUrls(), console.log(""), console.log(c.dim(` ${X} Watching content files for changes...`)), console.log("");
336
+ await r.listen(), r.printUrls(), console.log(""), console.log(
337
+ c.dim(` ${X} Watching content files for changes...`)
338
+ ), console.log("");
319
339
  }
320
- async function j(n) {
340
+ async function j(t) {
321
341
  const o = {
322
342
  locations: {},
323
343
  characters: {},
@@ -339,8 +359,8 @@ async function j(n) {
339
359
  { dir: "journal", key: "journalEntries" },
340
360
  { dir: "interludes", key: "interludes" }
341
361
  ];
342
- for (const { dir: t, key: a } of r) {
343
- const e = h(n, t);
362
+ for (const { dir: n, key: a } of r) {
363
+ const e = h(t, n);
344
364
  try {
345
365
  const i = await b(e);
346
366
  for (const l of i)
@@ -352,25 +372,25 @@ async function j(n) {
352
372
  }
353
373
  }
354
374
  try {
355
- const t = h(n, "locales"), a = await b(t);
375
+ const n = h(t, "locales"), a = await b(n);
356
376
  for (const e of a)
357
377
  if (f(e) === ".yaml" || f(e) === ".yml") {
358
- const i = h(t, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
378
+ const i = h(n, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
359
379
  o.locales[u] = d ?? {};
360
380
  }
361
381
  } catch {
362
382
  }
363
383
  try {
364
- const t = h(n, "dialogues"), a = await b(t);
384
+ const n = h(t, "dialogues"), a = await b(n);
365
385
  for (const e of a)
366
386
  if (f(e) === ".dlg") {
367
- const i = h(t, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
387
+ const i = h(n, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
368
388
  o.dialogues[u.id] = u;
369
389
  }
370
390
  } catch {
371
391
  }
372
392
  try {
373
- const t = h(n, "game.yaml"), a = await p(t, "utf-8");
393
+ const n = h(t, "game.yaml"), a = await p(n, "utf-8");
374
394
  s = y(a);
375
395
  } catch {
376
396
  console.warn(c.yellow(" No game.yaml found, using defaults")), s = {
@@ -383,15 +403,15 @@ async function j(n) {
383
403
  }
384
404
  return { registry: o, config: s };
385
405
  }
386
- async function te(n) {
406
+ async function ne(t) {
387
407
  try {
388
- const { registry: o, fileMap: s } = await oe(n), r = C(o, s);
408
+ const { registry: o, fileMap: s } = await oe(t), r = C(o, s);
389
409
  r.length > 0 && (console.log(""), I(r), console.log(""));
390
410
  } catch (o) {
391
411
  console.error(c.red(" Error running validation:"), o);
392
412
  }
393
413
  }
394
- async function oe(n) {
414
+ async function oe(t) {
395
415
  const o = {
396
416
  locations: {},
397
417
  characters: {},
@@ -411,8 +431,8 @@ async function oe(n) {
411
431
  { dir: "journal", key: "journalEntries" },
412
432
  { dir: "interludes", key: "interludes" }
413
433
  ];
414
- for (const { dir: t, key: a } of r) {
415
- const e = h(n, t);
434
+ for (const { dir: n, key: a } of r) {
435
+ const e = h(t, n);
416
436
  try {
417
437
  const i = await b(e);
418
438
  for (const l of i)
@@ -424,34 +444,34 @@ async function oe(n) {
424
444
  }
425
445
  }
426
446
  try {
427
- const t = h(n, "locales"), a = await b(t);
447
+ const n = h(t, "locales"), a = await b(n);
428
448
  for (const e of a)
429
449
  if (f(e) === ".yaml" || f(e) === ".yml") {
430
- const i = h(t, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
450
+ const i = h(n, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
431
451
  o.locales[u] = d ?? {};
432
452
  }
433
453
  } catch {
434
454
  }
435
455
  try {
436
- const t = h(n, "dialogues"), a = await b(t);
456
+ const n = h(t, "dialogues"), a = await b(n);
437
457
  for (const e of a)
438
458
  if (f(e) === ".dlg") {
439
- const i = h(t, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
459
+ const i = h(n, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
440
460
  o.dialogues[u.id] = u, s.set(u.id, E(process.cwd(), i));
441
461
  }
442
462
  } catch {
443
463
  }
444
464
  return { registry: o, fileMap: s };
445
465
  }
446
- function ae(n) {
447
- const o = `doodle-engine-assets-${n.version}`, s = [
448
- ...n.shell.map((t) => t.path),
449
- ...n.game.map((t) => t.path)
466
+ function ae(t) {
467
+ const o = `doodle-engine-assets-${t.version}`, s = [
468
+ ...t.shell.map((n) => n.path),
469
+ ...t.game.map((n) => n.path)
450
470
  ], r = JSON.stringify(s, null, 2);
451
471
  return `/**
452
472
  * Doodle Engine Service Worker
453
- * Generated at build time do not edit manually.
454
- * Cache version: ${n.version}
473
+ * Generated at build time. Do not edit manually.
474
+ * Cache version: ${t.version}
455
475
  */
456
476
 
457
477
  const CACHE_NAME = ${JSON.stringify(o)};
@@ -495,7 +515,7 @@ self.addEventListener('fetch', (event) => {
495
515
  return;
496
516
  }
497
517
 
498
- // Skip API and non-asset requests serve from network
518
+ // Skip API and non-asset requests. Serve from network.
499
519
  if (url.pathname.startsWith('/api/')) {
500
520
  return;
501
521
  }
@@ -519,34 +539,34 @@ self.addEventListener('fetch', (event) => {
519
539
  `;
520
540
  }
521
541
  async function re() {
522
- const n = process.cwd(), o = h(n, "content");
542
+ const t = process.cwd(), o = h(t, "content");
523
543
  console.log(""), console.log(c.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(c.dim("Validating content..."));
524
- let s, r, t;
544
+ let s, r, n;
525
545
  try {
526
546
  const a = await se(o);
527
- r = a.registry, t = a.config;
547
+ r = a.registry, n = a.config;
528
548
  const { fileMap: e } = a, i = C(r, e);
529
- I(i), i.length > 0 && (console.log(c.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), s = { registry: r, config: t };
549
+ I(i), i.length > 0 && (console.log(c.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), s = { registry: r, config: n };
530
550
  } catch (a) {
531
551
  console.error(c.red("Error loading content:"), a), process.exit(1);
532
552
  }
533
553
  console.log("");
534
554
  try {
535
555
  await P({
536
- root: n,
556
+ root: t,
537
557
  plugins: [G()],
538
558
  build: {
539
559
  outDir: "dist",
540
560
  emptyOutDir: !0
541
561
  }
542
562
  });
543
- const a = h(n, "dist"), e = n;
563
+ const a = h(t, "dist"), e = t;
544
564
  console.log(c.dim("Generating asset manifest..."));
545
565
  const i = await q(
546
- h(n, "assets"),
566
+ h(t, "assets"),
547
567
  e,
548
568
  r,
549
- t,
569
+ n,
550
570
  Date.now().toString()
551
571
  ), l = h(a, "api");
552
572
  await T(l, { recursive: !0 }), await w(h(l, "content"), JSON.stringify(s)), await w(h(l, "manifest"), JSON.stringify(i)), await w(
@@ -559,7 +579,7 @@ async function re() {
559
579
  console.error(c.red("Build failed:"), a), process.exit(1);
560
580
  }
561
581
  }
562
- async function se(n) {
582
+ async function se(t) {
563
583
  const o = {
564
584
  locations: {},
565
585
  characters: {},
@@ -580,7 +600,7 @@ async function se(n) {
580
600
  { dir: "interludes", key: "interludes" }
581
601
  ];
582
602
  for (const { dir: a, key: e } of r) {
583
- const i = h(n, a);
603
+ const i = h(t, a);
584
604
  try {
585
605
  const l = await b(i);
586
606
  for (const d of l)
@@ -592,7 +612,7 @@ async function se(n) {
592
612
  }
593
613
  }
594
614
  try {
595
- const a = h(n, "locales"), e = await b(a);
615
+ const a = h(t, "locales"), e = await b(a);
596
616
  for (const i of e)
597
617
  if (f(i) === ".yaml" || f(i) === ".yml") {
598
618
  const l = h(a, i), d = await p(l, "utf-8"), u = y(d), m = i.replace(/\.(yaml|yml)$/, "");
@@ -601,7 +621,7 @@ async function se(n) {
601
621
  } catch {
602
622
  }
603
623
  try {
604
- const a = h(n, "dialogues"), e = await b(a);
624
+ const a = h(t, "dialogues"), e = await b(a);
605
625
  for (const i of e)
606
626
  if (f(i) === ".dlg") {
607
627
  const l = h(a, i), d = await p(l, "utf-8"), u = i.replace(".dlg", ""), m = k(d, u);
@@ -609,12 +629,12 @@ async function se(n) {
609
629
  }
610
630
  } catch {
611
631
  }
612
- let t = null;
632
+ let n = null;
613
633
  try {
614
- const a = h(n, "game.yaml"), e = await p(a, "utf-8");
615
- t = y(e);
634
+ const a = h(t, "game.yaml"), e = await p(a, "utf-8");
635
+ n = y(e);
616
636
  } catch {
617
- t = {
637
+ n = {
618
638
  id: "game",
619
639
  startLocation: "",
620
640
  startTime: { day: 1, hour: 8 },
@@ -623,19 +643,19 @@ async function se(n) {
623
643
  startInventory: []
624
644
  };
625
645
  }
626
- return { registry: o, fileMap: s, config: t };
646
+ return { registry: o, fileMap: s, config: n };
627
647
  }
628
648
  async function ie() {
629
- const n = process.cwd(), o = h(n, "content");
649
+ const t = process.cwd(), o = h(t, "content");
630
650
  console.log(""), console.log(c.bold.magenta("🐾 Validating Doodle Engine content...")), console.log("");
631
651
  try {
632
- const { registry: s, fileMap: r } = await ce(o), t = C(s, r);
633
- I(t), t.length > 0 && process.exit(1);
652
+ const { registry: s, fileMap: r } = await ce(o), n = C(s, r);
653
+ I(n), n.length > 0 && process.exit(1);
634
654
  } catch (s) {
635
655
  console.error(c.red("Error loading content:"), s), process.exit(1);
636
656
  }
637
657
  }
638
- async function ce(n) {
658
+ async function ce(t) {
639
659
  const o = {
640
660
  locations: {},
641
661
  characters: {},
@@ -653,8 +673,8 @@ async function ce(n) {
653
673
  { dir: "quests", key: "quests" },
654
674
  { dir: "journal", key: "journalEntries" }
655
675
  ];
656
- for (const { dir: t, key: a } of r) {
657
- const e = h(n, t);
676
+ for (const { dir: n, key: a } of r) {
677
+ const e = h(t, n);
658
678
  try {
659
679
  const i = await b(e);
660
680
  for (const l of i)
@@ -666,19 +686,19 @@ async function ce(n) {
666
686
  }
667
687
  }
668
688
  try {
669
- const t = h(n, "locales"), a = await b(t);
689
+ const n = h(t, "locales"), a = await b(n);
670
690
  for (const e of a)
671
691
  if (f(e) === ".yaml" || f(e) === ".yml") {
672
- const i = h(t, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
692
+ const i = h(n, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
673
693
  o.locales[u] = d ?? {};
674
694
  }
675
695
  } catch {
676
696
  }
677
697
  try {
678
- const t = h(n, "dialogues"), a = await b(t);
698
+ const n = h(t, "dialogues"), a = await b(n);
679
699
  for (const e of a)
680
700
  if (f(e) === ".dlg") {
681
- const i = h(t, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
701
+ const i = h(n, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
682
702
  o.dialogues[u.id] = u, s.set(u.id, E(process.cwd(), i));
683
703
  }
684
704
  } catch {
@@ -691,33 +711,33 @@ dist
691
711
  *.log
692
712
  `, de = `<!doctype html>
693
713
  <html lang="en">
694
- <head>
695
- <meta charset="UTF-8" />
696
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
697
- <title>Doodle Engine Game</title>
698
- </head>
699
- <body>
700
- <div id="root"></div>
701
- <script type="module" src="/src/main.tsx"><\/script>
702
- </body>
714
+ <head>
715
+ <meta charset="UTF-8" />
716
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
717
+ <title>Doodle Engine Game</title>
718
+ </head>
719
+ <body>
720
+ <div id="root"></div>
721
+ <script type="module" src="/src/main.tsx"><\/script>
722
+ </body>
703
723
  </html>
704
724
  `, ue = `{
705
- "compilerOptions": {
706
- "target": "ES2024",
707
- "lib": ["ES2024", "DOM", "DOM.Iterable"],
708
- "module": "ESNext",
709
- "moduleResolution": "bundler",
710
- "jsx": "react-jsx",
711
- "strict": true,
712
- "skipLibCheck": true,
713
- "esModuleInterop": true,
714
- "forceConsistentCasingInFileNames": true,
715
- "resolveJsonModule": true,
716
- "isolatedModules": true,
717
- "noEmit": true,
718
- "types": ["vite/client"]
719
- },
720
- "include": ["src"]
725
+ "compilerOptions": {
726
+ "target": "ES2024",
727
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
728
+ "module": "ESNext",
729
+ "moduleResolution": "bundler",
730
+ "jsx": "react-jsx",
731
+ "strict": true,
732
+ "skipLibCheck": true,
733
+ "esModuleInterop": true,
734
+ "forceConsistentCasingInFileNames": true,
735
+ "resolveJsonModule": true,
736
+ "isolatedModules": true,
737
+ "noEmit": true,
738
+ "types": ["vite/client"]
739
+ },
740
+ "include": ["src"]
721
741
  }
722
742
  `, he = `id: bartender
723
743
  name: "@character.bartender.name"
@@ -734,20 +754,20 @@ location: market
734
754
  dialogue: merchant_intro
735
755
  stats: {}
736
756
  `, ge = `# This dialogue is triggered by clicking the bartender character.
737
- # SPEAKER: lines set who's talking matched to character ID (case-insensitive).
757
+ # SPEAKER: lines set who's talking, matched to character ID (case-insensitive).
738
758
  # Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
739
759
 
740
760
  NODE start
741
761
  BARTENDER: @bartender.greeting
742
762
 
743
- # Always available ask for rumors (demonstrates: flag, relationship, journalEntry)
763
+ # Always available: ask for rumors (demonstrates: flag, relationship, journalEntry)
744
764
  CHOICE @bartender.choice.whats_news
745
765
  SET flag metBartender
746
766
  ADD relationship bartender 1
747
767
  GOTO rumors
748
768
  END
749
769
 
750
- # Always available buy a drink (demonstrates: variable change, flag)
770
+ # Always available: buy a drink (demonstrates: variable change, flag)
751
771
  CHOICE @bartender.choice.order_drink
752
772
  GOTO order_drink
753
773
  END
@@ -810,7 +830,7 @@ NODE order_drink
810
830
  GOTO after_drink
811
831
  END
812
832
 
813
- # Try to bluff a free drink rolls dice inline (demonstrates: ROLL + GOTO within one dialogue)
833
+ # Try to bluff a free drink. Rolls dice inline (demonstrates: ROLL + GOTO within one dialogue)
814
834
  CHOICE @bartender.choice.try_bluff
815
835
  GOTO bluff_attempt
816
836
  END
@@ -821,7 +841,7 @@ NODE order_drink
821
841
  END
822
842
 
823
843
  NODE bluff_attempt
824
- # Roll happens immediately on entering this node — auto-advances to success or failure
844
+ # Roll happens immediately on entering this node. Auto-advances to success or failure.
825
845
  ROLL bluffRoll 1 20
826
846
  IF variableGreaterThan bluffRoll 14
827
847
  GOTO bluff_success
@@ -915,11 +935,11 @@ NODE work_done
915
935
  NODE farewell
916
936
  BARTENDER: @bartender.farewell
917
937
  END dialogue
918
- `, fe = `# Standalone skill-check demo demonstrates dice syntax in isolation.
938
+ `, fe = `# Standalone skill-check demo. Demonstrates dice syntax in isolation.
919
939
  #
920
- # ROLL <variable> <min> <max> rolls a random integer and stores it in a variable.
921
- # {varName} in dialogue text, replaced with the variable's value.
922
- # roll <min> <max> <threshold> condition: rolls and returns true if result >= threshold.
940
+ # ROLL <variable> <min> <max> : rolls a random integer and stores it in a variable.
941
+ # {varName} : in dialogue text, replaced with the variable's value.
942
+ # roll <min> <max> <threshold> : condition. Rolls and returns true if result >= threshold.
923
943
  #
924
944
  # The actual bluff check in this scaffold is inlined into bartender_greeting.dlg
925
945
  # so it can flow back into the bartender conversation (GOTO after_drink, GOTO start).
@@ -967,7 +987,7 @@ NODE failure
967
987
  NARRATOR: @bluff.failure.narration
968
988
  BARTENDER: @bluff.failure.bartender
969
989
 
970
- # Still have to pay or just walk away
990
+ # Still have to pay, or just walk away
971
991
  CHOICE @bluff.choice.fine_pay
972
992
  REQUIRE variableGreaterThan gold 4
973
993
  ADD variable gold -5
@@ -1027,7 +1047,7 @@ NODE start
1027
1047
  NODE browse
1028
1048
  MERCHANT: @merchant.browse
1029
1049
 
1030
- # Conditional purchase need enough gold (demonstrates: variableGreaterThan)
1050
+ # Conditional purchase: need enough gold (demonstrates: variableGreaterThan)
1031
1051
  CHOICE @merchant.choice.buy_map
1032
1052
  REQUIRE variableGreaterThan gold 19
1033
1053
  ADD variable gold -20
@@ -1106,7 +1126,7 @@ REQUIRE notFlag seenTavernIntro
1106
1126
 
1107
1127
  # Each NODE is a conversation point. The first NODE is always the start.
1108
1128
  NODE start
1109
- # NARRATOR: has no speaker used for scene-setting text.
1129
+ # NARRATOR: has no speaker. Used for scene-setting text.
1110
1130
  # @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
1111
1131
  # You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
1112
1132
  NARRATOR: @narrator.tavern_intro
@@ -1115,7 +1135,7 @@ NODE start
1115
1135
  SET flag seenTavernIntro
1116
1136
 
1117
1137
  # CHOICE text can use a @key or "inline text".
1118
- # A choice with END dialogue is a terminal choice no GOTO needed.
1138
+ # A choice with END dialogue is a terminal choice. No GOTO needed.
1119
1139
  CHOICE @narrator.choice.look_around
1120
1140
  END dialogue
1121
1141
  END
@@ -1123,7 +1143,7 @@ NODE start
1123
1143
 
1124
1144
  # Shell screen configuration
1125
1145
  # Uncomment and provide your own assets to customize the game shell.
1126
- # The game will work without these screens will use styled gradients and skip sounds.
1146
+ # The game will work without these. Screens will use styled gradients and skip sounds.
1127
1147
  #
1128
1148
  # shell:
1129
1149
  # splash:
@@ -1183,7 +1203,7 @@ text: |
1183
1203
 
1184
1204
  # Auto-trigger when the player enters the tavern for the first time.
1185
1205
  # triggerConditions prevents re-triggering. effects runs when the interlude fires
1186
- # setFlag here marks it seen so it won't show again on return visits.
1206
+ # setFlag here marks it seen so it won't show again on return visits.
1187
1207
  triggerLocation: tavern
1188
1208
  triggerConditions:
1189
1209
  - type: notFlag
@@ -1213,7 +1233,7 @@ category: places
1213
1233
  `, De = `# ===================
1214
1234
  # Narrator Intros
1215
1235
  # ===================
1216
- 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."
1236
+ 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."
1217
1237
  narrator.market_intro: "The market square opens up before you, a riot of color and noise. Stalls line every side, draped in bright awnings. Merchants call out their prices, children dart between carts, and somewhere a street musician plays an out-of-tune fiddle."
1218
1238
  narrator.choice.look_around: "Look around."
1219
1239
 
@@ -1259,7 +1279,7 @@ quest.odd_jobs.stage.complete: "Job well done. Elena paid 50 gold for the troubl
1259
1279
  journal.tavern_discovery.title: "The Salty Dog"
1260
1280
  journal.tavern_discovery.text: "I found a tavern in the docks district called The Salty Dog. The bartender, Marcus, seems well-connected. Word is there's been strange folk around the docks at night."
1261
1281
  journal.odd_jobs_accepted.title: "Work at the Market"
1262
- journal.odd_jobs_accepted.text: "Marcus pointed me toward a merchant in the market square Elena. She's looking for someone reliable. Should head over and introduce myself."
1282
+ journal.odd_jobs_accepted.text: "Marcus pointed me toward a merchant in the market square: Elena. She's looking for someone reliable. Should head over and introduce myself."
1263
1283
  journal.market_square.title: "Market Square"
1264
1284
  journal.market_square.text: "The market square is the heart of this little town. Elena has been trading here for fifteen years. A good place to resupply."
1265
1285
 
@@ -1290,15 +1310,15 @@ bartender.choice.got_it: "Got it. I'll take care of it."
1290
1310
  bartender.choice.anytime: "Anytime."
1291
1311
 
1292
1312
  # Responses
1293
- bartender.rumors: "Word is there's been strange folk poking around the docks at night. And the merchant in the market square has been looking for hired help. Oh found this on the floor the other day. Strange markings. You can have it."
1313
+ bartender.rumors: "Word is there's been strange folk poking around the docks at night. And the merchant in the market square has been looking for hired help. Oh, found this on the floor the other day. Strange markings. You can have it."
1294
1314
  bartender.rumors_detail: "Some say they've seen lights out on the old pier after midnight. Probably smugglers, but who knows these days. Keep your wits about you."
1295
- bartender.order_drink: "Five gold for the house special strongest thing this side of the river. What do you say?"
1315
+ bartender.order_drink: "Five gold for the house special, strongest thing this side of the river. What do you say?"
1296
1316
  bartender.after_drink: "Glad you like it! Brewed it myself. Now then, anything else?"
1297
- bartender.work_intro: "Well now, you look capable enough. There's a merchant over in the market square Elena she's been asking around for someone reliable. Tell her Marcus sent you."
1317
+ bartender.work_intro: "Well now, you look capable enough. There's a merchant over in the market square. Elena has been asking around for someone reliable. Tell her Marcus sent you."
1298
1318
  bartender.work_accepted: "Good on you. Elena's fair with pay. Head to the market square when you're ready."
1299
- bartender.work_details: "Something about a shipment that needs escorting. Nothing too dangerous, she says but then, that's what they always say."
1319
+ bartender.work_details: "Something about a shipment that needs escorting. Nothing too dangerous, she says, but then, that's what they always say."
1300
1320
  bartender.work_followup: "Still working on that job for Elena? She's over at the market square if you haven't found her yet. Don't keep her waiting too long."
1301
- bartender.work_done: "I heard Elena's singing your praises. Good work out there I knew you had it in you."
1321
+ bartender.work_done: "I heard Elena's singing your praises. Good work out there. I knew you had it in you."
1302
1322
 
1303
1323
  # ===================
1304
1324
  # Merchant Dialogue
@@ -1322,12 +1342,12 @@ merchant.choice.on_it: "Consider it done."
1322
1342
  merchant.choice.glad_to_help: "Glad I could help."
1323
1343
 
1324
1344
  # Responses
1325
- merchant.browse: "Take a look! I've got a fine map of the area if you're new around here. Only twenty gold a bargain for not getting lost."
1345
+ merchant.browse: "Take a look! I've got a fine map of the area if you're new around here. Only twenty gold, a bargain for not getting lost."
1326
1346
  merchant.sold_map: "Excellent choice! This'll keep you from wandering into the wrong part of town."
1327
- merchant.too_pricey: "Ha! You'd pay twice that if you got lost in the docks at night. But no rush I'll be here."
1347
+ merchant.too_pricey: "Ha! You'd pay twice that if you got lost in the docks at night. But no rush. I'll be here."
1328
1348
  merchant.odd_jobs: "Ah, Marcus sent you? Good man. I've got a shipment coming in and could use someone to keep an eye on things. Interested?"
1329
- merchant.task_details: "Head down to the docks at sundown. You'll meet my contact there a woman named Ria. Make sure the cargo gets here in one piece."
1330
- merchant.quest_complete: "Everything arrived in perfect condition! You've earned this fifty gold, as promised. If I need help again, you'll be the first I call."
1349
+ merchant.task_details: "Head down to the docks at sundown. You'll meet my contact there, a woman named Ria. Make sure the cargo gets here in one piece."
1350
+ merchant.quest_complete: "Everything arrived in perfect condition! You've earned this: fifty gold, as promised. If I need help again, you'll be the first I call."
1331
1351
  merchant.about_market: "Market Square is the heart of this little town. You can find just about anything here if you know where to look. I've been trading here for fifteen years."
1332
1352
 
1333
1353
  # ===================
@@ -1347,7 +1367,7 @@ bluff.setup: "Marcus eyes you across the bar. You consider spinning him a tale t
1347
1367
  bluff.choice.attempt: "Try to bluff him."
1348
1368
  bluff.choice.back_down: "Actually, never mind."
1349
1369
  bluff.backed_down: "Some fights aren't worth picking."
1350
- bluff.rolled: "You spin the tale with {bluffRoll} on your roll and Marcus listens carefully."
1370
+ bluff.rolled: "You spin the tale with {bluffRoll} on your roll, and Marcus listens carefully."
1351
1371
  bluff.success.narration: "The story lands perfectly. Marcus laughs and slides a free drink across the bar."
1352
1372
  bluff.success.bartender: "Sure, this one's on the house."
1353
1373
  bluff.failure.narration: "Marcus raises an eyebrow, entirely unconvinced."
@@ -1388,189 +1408,190 @@ stages:
1388
1408
  description: "@quest.odd_jobs.stage.talked_to_merchant"
1389
1409
  - id: complete
1390
1410
  description: "@quest.odd_jobs.stage.complete"
1391
- `, Re = `import { useEffect, useState } from "react";
1392
- import { Engine } from "@doodle-engine/core";
1393
- import type { GameState, Snapshot } from "@doodle-engine/core";
1394
- import { GameProvider, useGame } from "@doodle-engine/react";
1411
+ `, Re = `import { useEffect, useState } from 'react';
1412
+ import { Engine } from '@doodle-engine/core';
1413
+ import type { GameState, Snapshot } from '@doodle-engine/core';
1414
+ import { GameProvider, useGame } from '@doodle-engine/react';
1395
1415
 
1396
1416
  export function App() {
1397
- const [game, setGame] = useState<{
1398
- engine: Engine;
1399
- snapshot: Snapshot;
1400
- } | null>(null);
1401
-
1402
- useEffect(() => {
1403
- fetch("/api/content")
1404
- .then((res) => res.json())
1405
- .then((data) => {
1406
- const engine = new Engine(data.registry, createEmptyState());
1407
- const snapshot = engine.newGame(data.config);
1408
- setGame({ engine, snapshot });
1409
- });
1410
- }, []);
1417
+ const [game, setGame] = useState<{
1418
+ engine: Engine;
1419
+ snapshot: Snapshot;
1420
+ } | null>(null);
1421
+
1422
+ useEffect(() => {
1423
+ fetch('/api/content')
1424
+ .then((res) => res.json())
1425
+ .then((data) => {
1426
+ const engine = new Engine(data.registry, createEmptyState());
1427
+ const snapshot = engine.newGame(data.config);
1428
+ setGame({ engine, snapshot });
1429
+ });
1430
+ }, []);
1431
+
1432
+ if (!game) {
1433
+ return (
1434
+ <div className="app-bootstrap">
1435
+ <div className="spinner" />
1436
+ </div>
1437
+ );
1438
+ }
1411
1439
 
1412
- if (!game) {
1413
1440
  return (
1414
- <div className="app-bootstrap">
1415
- <div className="spinner" />
1416
- </div>
1441
+ <GameProvider
1442
+ engine={game.engine}
1443
+ initialSnapshot={game.snapshot}
1444
+ devTools={import.meta.env.DEV}
1445
+ >
1446
+ <GameUI />
1447
+ </GameProvider>
1417
1448
  );
1418
- }
1419
-
1420
- return (
1421
- <GameProvider
1422
- engine={game.engine}
1423
- initialSnapshot={game.snapshot}
1424
- devTools={import.meta.env.DEV}
1425
- >
1426
- <GameUI />
1427
- </GameProvider>
1428
- );
1429
1449
  }
1430
1450
 
1431
1451
  function GameUI() {
1432
- const { snapshot, actions } = useGame();
1433
-
1434
- return (
1435
- <div
1436
- style={{
1437
- padding: "2rem",
1438
- fontFamily: "sans-serif",
1439
- maxWidth: "800px",
1440
- margin: "0 auto",
1441
- }}
1442
- >
1443
- <h1>{snapshot.location.name}</h1>
1444
- <p>{snapshot.location.description}</p>
1445
-
1446
- {snapshot.dialogue && (
1452
+ const { snapshot, actions } = useGame();
1453
+
1454
+ return (
1447
1455
  <div
1448
- style={{
1449
- background: "#f0f0f0",
1450
- padding: "1rem",
1451
- borderRadius: "8px",
1452
- margin: "1rem 0",
1453
- }}
1456
+ style={{
1457
+ padding: '2rem',
1458
+ fontFamily: 'sans-serif',
1459
+ maxWidth: '800px',
1460
+ margin: '0 auto',
1461
+ }}
1454
1462
  >
1455
- <strong>{snapshot.dialogue.speakerName}:</strong>
1456
- <p>{snapshot.dialogue.text}</p>
1457
- {snapshot.choices.map((choice) => (
1458
- <button
1459
- key={choice.id}
1460
- onClick={() => actions.selectChoice(choice.id)}
1461
- style={{
1462
- display: "block",
1463
- margin: "0.5rem 0",
1464
- padding: "0.5rem 1rem",
1465
- cursor: "pointer",
1466
- }}
1467
- >
1468
- {choice.text}
1469
- </button>
1470
- ))}
1471
- </div>
1472
- )}
1473
-
1474
- {!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
1475
- <div>
1476
- <h2>Characters here</h2>
1477
- {snapshot.charactersHere.map((char) => (
1478
- <button
1479
- key={char.id}
1480
- onClick={() => actions.talkTo(char.id)}
1481
- style={{
1482
- display: "block",
1483
- margin: "0.5rem 0",
1484
- padding: "0.5rem 1rem",
1485
- cursor: "pointer",
1486
- }}
1487
- >
1488
- Talk to {char.name}
1489
- </button>
1490
- ))}
1463
+ <h1>{snapshot.location.name}</h1>
1464
+ <p>{snapshot.location.description}</p>
1465
+
1466
+ {snapshot.dialogue && (
1467
+ <div
1468
+ style={{
1469
+ background: '#f0f0f0',
1470
+ padding: '1rem',
1471
+ borderRadius: '8px',
1472
+ margin: '1rem 0',
1473
+ }}
1474
+ >
1475
+ <strong>{snapshot.dialogue.speakerName}:</strong>
1476
+ <p>{snapshot.dialogue.text}</p>
1477
+ {snapshot.choices.map((choice) => (
1478
+ <button
1479
+ key={choice.id}
1480
+ onClick={() => actions.selectChoice(choice.id)}
1481
+ style={{
1482
+ display: 'block',
1483
+ margin: '0.5rem 0',
1484
+ padding: '0.5rem 1rem',
1485
+ cursor: 'pointer',
1486
+ }}
1487
+ >
1488
+ {choice.text}
1489
+ </button>
1490
+ ))}
1491
+ </div>
1492
+ )}
1493
+
1494
+ {!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
1495
+ <div>
1496
+ <h2>Characters here</h2>
1497
+ {snapshot.charactersHere.map((char) => (
1498
+ <button
1499
+ key={char.id}
1500
+ onClick={() => actions.talkTo(char.id)}
1501
+ style={{
1502
+ display: 'block',
1503
+ margin: '0.5rem 0',
1504
+ padding: '0.5rem 1rem',
1505
+ cursor: 'pointer',
1506
+ }}
1507
+ >
1508
+ Talk to {char.name}
1509
+ </button>
1510
+ ))}
1511
+ </div>
1512
+ )}
1491
1513
  </div>
1492
- )}
1493
- </div>
1494
- );
1514
+ );
1495
1515
  }
1496
1516
 
1497
1517
  function createEmptyState(): GameState {
1498
- return {
1499
- currentLocation: "",
1500
- currentTime: { day: 1, hour: 0 },
1501
- flags: {},
1502
- variables: {},
1503
- inventory: [],
1504
- questProgress: {},
1505
- unlockedJournalEntries: [],
1506
- playerNotes: [],
1507
- dialogueState: null,
1508
- characterState: {},
1509
- itemLocations: {},
1510
- mapEnabled: true,
1511
- notifications: [],
1512
- pendingSounds: [],
1513
- pendingVideo: null,
1514
- pendingInterlude: null,
1515
- currentLocale: "en",
1516
- };
1518
+ return {
1519
+ currentLocation: '',
1520
+ currentTime: { day: 1, hour: 0 },
1521
+ flags: {},
1522
+ variables: {},
1523
+ inventory: [],
1524
+ questProgress: {},
1525
+ unlockedJournalEntries: [],
1526
+ playerNotes: [],
1527
+ dialogueState: null,
1528
+ characterState: {},
1529
+ itemLocations: {},
1530
+ mapEnabled: true,
1531
+ notifications: [],
1532
+ pendingSounds: [],
1533
+ pendingVideo: null,
1534
+ pendingInterlude: null,
1535
+ currentLocale: 'en',
1536
+ };
1517
1537
  }
1518
- `, Ae = `import { useEffect, useState } from "react";
1538
+ `, Ae = `import { useEffect, useState } from 'react';
1519
1539
  import type {
1520
- ContentRegistry,
1521
- GameConfig,
1522
- AssetManifest,
1523
- } from "@doodle-engine/core";
1524
- import { GameShell } from "@doodle-engine/react";
1540
+ ContentRegistry,
1541
+ GameConfig,
1542
+ AssetManifest,
1543
+ } from '@doodle-engine/core';
1544
+ import { GameShell } from '@doodle-engine/react';
1525
1545
 
1526
1546
  export function App() {
1527
- const [content, setContent] = useState<{
1528
- registry: ContentRegistry;
1529
- config: GameConfig;
1530
- } | null>(null);
1531
- const [manifest, setManifest] = useState<AssetManifest | null>(null);
1532
-
1533
- useEffect(() => {
1534
- Promise.all([
1535
- fetch("/api/content").then((res) => res.json()),
1536
- fetch("/api/manifest").then((res) => res.json()),
1537
- ]).then(([contentData, manifestData]) => {
1538
- setContent({
1539
- registry: contentData.registry,
1540
- config: contentData.config,
1541
- });
1542
- setManifest(manifestData);
1543
- });
1544
- }, []);
1547
+ const [content, setContent] = useState<{
1548
+ registry: ContentRegistry;
1549
+ config: GameConfig;
1550
+ } | null>(null);
1551
+ const [manifest, setManifest] = useState<AssetManifest | null>(null);
1552
+
1553
+ useEffect(() => {
1554
+ Promise.all([
1555
+ fetch('/api/content').then((res) => res.json()),
1556
+ fetch('/api/manifest').then((res) => res.json()),
1557
+ ]).then(([contentData, manifestData]) => {
1558
+ setContent({
1559
+ registry: contentData.registry,
1560
+ config: contentData.config,
1561
+ });
1562
+ setManifest(manifestData);
1563
+ });
1564
+ }, []);
1565
+
1566
+ // Minimal bootstrap state while fetching manifest/content
1567
+ if (!content || !manifest) {
1568
+ return (
1569
+ <div className="app-bootstrap">
1570
+ <div className="spinner" />
1571
+ </div>
1572
+ );
1573
+ }
1545
1574
 
1546
- // Minimal bootstrap state while fetching manifest/content
1547
- if (!content || !manifest) {
1548
1575
  return (
1549
- <div className="app-bootstrap">
1550
- <div className="spinner" />
1551
- </div>
1576
+ <GameShell
1577
+ registry={content.registry}
1578
+ config={content.config}
1579
+ manifest={manifest}
1580
+ title="My Doodle Game"
1581
+ subtitle="A text-based adventure"
1582
+ availableLocales={[{ code: 'en', label: 'English' }]}
1583
+ devTools={import.meta.env.DEV}
1584
+ />
1552
1585
  );
1553
- }
1554
-
1555
- return (
1556
- <GameShell
1557
- registry={content.registry}
1558
- config={content.config}
1559
- manifest={manifest}
1560
- title="My Doodle Game"
1561
- subtitle="A text-based adventure"
1562
- availableLocales={[{ code: "en", label: "English" }]}
1563
- devTools={import.meta.env.DEV}
1564
- />
1565
- );
1566
1586
  }
1567
1587
  `, Se = `body {
1568
- margin: 0;
1569
- font-family:
1570
- -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
1571
- "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
1572
- -webkit-font-smoothing: antialiased;
1573
- -moz-osx-font-smoothing: grayscale;
1588
+ margin: 0;
1589
+ font-family:
1590
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
1591
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
1592
+ sans-serif;
1593
+ -webkit-font-smoothing: antialiased;
1594
+ -moz-osx-font-smoothing: grayscale;
1574
1595
  }
1575
1596
 
1576
1597
  /* ── Theme overrides ──────────────────────────────────────────────── */
@@ -1584,23 +1605,23 @@ export function App() {
1584
1605
  --doodle-accent: #6366f1;
1585
1606
  }
1586
1607
  */
1587
- `, je = `import { StrictMode } from "react";
1588
- import { createRoot } from "react-dom/client";
1589
- import "@doodle-engine/react/style.css";
1590
- import { App } from "./App";
1591
- import "./index.css";
1608
+ `, je = `import { StrictMode } from 'react';
1609
+ import { createRoot } from 'react-dom/client';
1610
+ import '@doodle-engine/react/style.css';
1611
+ import { App } from './App';
1612
+ import './index.css';
1592
1613
 
1593
1614
  // Register service worker in production for offline asset caching
1594
- if ("serviceWorker" in navigator && import.meta.env.PROD) {
1595
- navigator.serviceWorker.register("/sw.js").catch(() => {
1596
- // SW registration failure is non-fatal
1597
- });
1615
+ if ('serviceWorker' in navigator && import.meta.env.PROD) {
1616
+ navigator.serviceWorker.register('/sw.js').catch(() => {
1617
+ // SW registration failure is non-fatal
1618
+ });
1598
1619
  }
1599
1620
 
1600
- createRoot(document.getElementById("root")!).render(
1601
- <StrictMode>
1602
- <App />
1603
- </StrictMode>,
1621
+ createRoot(document.getElementById('root')!).render(
1622
+ <StrictMode>
1623
+ <App />
1624
+ </StrictMode>
1604
1625
  );
1605
1626
  `, D = "🐾", xe = "🐕", H = "🦴", Ge = "✨", $ = "📁", N = "✅", qe = "🚀", x = /* @__PURE__ */ Object.assign({
1606
1627
  "./templates/_root/_gitignore": le,
@@ -1629,8 +1650,8 @@ createRoot(document.getElementById("root")!).render(
1629
1650
  "./templates/src/index.css": Se,
1630
1651
  "./templates/src/main.tsx": je
1631
1652
  });
1632
- function He(n) {
1633
- const o = n.replace("./templates/", "");
1653
+ function He(t) {
1654
+ const o = t.replace("./templates/", "");
1634
1655
  if (o === "src/App.default.tsx" || o === "src/App.custom.tsx")
1635
1656
  return null;
1636
1657
  if (o.startsWith("_root/")) {
@@ -1639,9 +1660,9 @@ function He(n) {
1639
1660
  }
1640
1661
  return o;
1641
1662
  }
1642
- async function $e(n) {
1643
- const o = h(process.cwd(), n);
1644
- 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(` ${xe} Creating new game: ${c.bold.cyan(n)}`), console.log("");
1663
+ async function $e(t) {
1664
+ const o = h(process.cwd(), t);
1665
+ 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(` ${xe} Creating new game: ${c.bold.cyan(t)}`), console.log("");
1645
1666
  const { useDefaultRenderer: s } = await J({
1646
1667
  type: "confirm",
1647
1668
  name: "useDefaultRenderer",
@@ -1651,13 +1672,13 @@ async function $e(n) {
1651
1672
  s === void 0 && (console.log(
1652
1673
  c.yellow(`
1653
1674
  ${H} No worries, maybe next time! Woof!`)
1654
- ), process.exit(0)), console.log(""), await Me(o, n, s), console.log(""), console.log(c.bold.green(` ${N} Project created successfully!`)), console.log(""), console.log(c.dim(` ${$} ${o}`)), console.log(""), console.log(c.bold(" Next steps:")), console.log(c.cyan(` cd ${n}`)), console.log(
1675
+ ), process.exit(0)), console.log(""), await Me(o, t, s), console.log(""), console.log(c.bold.green(` ${N} Project created successfully!`)), console.log(""), console.log(c.dim(` ${$} ${o}`)), console.log(""), console.log(c.bold(" Next steps:")), console.log(c.cyan(` cd ${t}`)), console.log(
1655
1676
  c.cyan(" npm install ") + c.dim("# or: yarn install / pnpm install")
1656
1677
  ), console.log(
1657
1678
  c.cyan(" npm run dev ") + c.dim("# or: yarn dev / pnpm dev")
1658
1679
  ), console.log(""), console.log(c.dim(` ${qe} Happy game making! ${D}`)), console.log("");
1659
1680
  }
1660
- async function Me(n, o, s) {
1681
+ async function Me(t, o, s) {
1661
1682
  const r = [
1662
1683
  "content/locations",
1663
1684
  "content/characters",
@@ -1679,15 +1700,16 @@ async function Me(n, o, s) {
1679
1700
  ];
1680
1701
  console.log(` ${$} ${c.bold("Creating directories...")}`);
1681
1702
  for (const e of r)
1682
- await T(h(n, e), { recursive: !0 });
1703
+ await T(h(t, e), { recursive: !0 });
1683
1704
  console.log(c.green(` ${N} Directories created`)), console.log("");
1684
- const t = {
1705
+ const n = {
1685
1706
  name: o,
1686
1707
  version: "0.1.0",
1687
1708
  type: "module",
1688
1709
  scripts: {
1689
1710
  dev: "doodle dev",
1690
1711
  build: "doodle build",
1712
+ validate: "doodle validate",
1691
1713
  preview: "vite preview"
1692
1714
  },
1693
1715
  dependencies: {
@@ -1706,17 +1728,17 @@ async function Me(n, o, s) {
1706
1728
  }
1707
1729
  };
1708
1730
  console.log(` ${Ge} ${c.bold("Writing project files...")}`), await w(
1709
- h(n, "package.json"),
1710
- JSON.stringify(t, null, 2)
1731
+ h(t, "package.json"),
1732
+ JSON.stringify(n, null, 2)
1711
1733
  );
1712
1734
  for (const [e, i] of Object.entries(x)) {
1713
1735
  const l = He(e);
1714
1736
  if (l === null) continue;
1715
- const d = h(n, l);
1737
+ const d = h(t, l);
1716
1738
  await T(U(d), { recursive: !0 }), await w(d, i);
1717
1739
  }
1718
1740
  const a = s ? "./templates/src/App.default.tsx" : "./templates/src/App.custom.tsx";
1719
- await w(h(n, "src/App.tsx"), x[a]), console.log(c.green(` ${N} Source files created`)), console.log(""), console.log(` ${H} ${c.bold("Starter content written")}`), 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(
1741
+ await w(h(t, "src/App.tsx"), x[a]), console.log(c.green(` ${N} Source files created`)), console.log(""), console.log(` ${H} ${c.bold("Starter content written")}`), 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(
1720
1742
  c.dim(" 1 interlude (chapter one, auto-triggers at tavern)")
1721
1743
  ), console.log(
1722
1744
  c.dim(
@@ -1726,13 +1748,13 @@ async function Me(n, o, s) {
1726
1748
  }
1727
1749
  const v = new M();
1728
1750
  v.name("doodle").description(
1729
- c.magenta("🐾 Doodle Engine") + c.dim(" Narrative RPG development tools")
1751
+ c.magenta("🐾 Doodle Engine") + c.dim(": Narrative RPG development tools")
1730
1752
  ).version("0.0.1");
1731
- v.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (n) => {
1732
- await $e(n);
1753
+ v.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (t) => {
1754
+ await $e(t);
1733
1755
  });
1734
1756
  v.command("dev").description("Start development server with hot reload").action(async () => {
1735
- await ne();
1757
+ await te();
1736
1758
  });
1737
1759
  v.command("build").description("Build game for production").action(async () => {
1738
1760
  await re();