@doodle-engine/cli 0.0.8 → 0.0.9

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