@doodle-engine/cli 0.0.1 → 0.0.3

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,55 +1,56 @@
1
1
  #!/usr/bin/env node
2
- import { Command as $ } from "commander";
3
- import { crayon as c } from "crayon.js";
4
- import { createServer as k, build as C } from "vite";
5
- import b from "@vitejs/plugin-react";
6
- import { watch as D } from "chokidar";
7
- import { readdir as w, readFile as p } from "fs/promises";
8
- import { join as i, extname as u } from "path";
9
- import { parse as h } from "yaml";
10
- import { parseDialogue as P } from "@doodle-engine/core";
11
- const v = "🐾", E = "✨", S = "✏️", j = "➕";
12
- async function q() {
13
- const r = process.cwd(), s = i(r, "content");
14
- console.log(""), console.log(c.bold.magenta(` ${v} Doodle Engine Dev Server ${v}`)), console.log("");
2
+ import { Command as C } from "commander";
3
+ import { crayon as t } from "crayon.js";
4
+ import { createServer as T, build as N } from "vite";
5
+ import O from "@vitejs/plugin-react";
6
+ import { watch as I } from "chokidar";
7
+ import { readdir as _, readFile as y, mkdir as S, writeFile as a } from "fs/promises";
8
+ import { join as o, extname as g } from "path";
9
+ import { parse as w } from "yaml";
10
+ import { parseDialogue as R } from "@doodle-engine/core";
11
+ import A from "prompts";
12
+ const E = "🐾", G = "✨", q = "✏️", H = "➕";
13
+ async function j() {
14
+ const e = process.cwd(), s = o(e, "content");
15
+ console.log(""), console.log(t.bold.magenta(` ${E} Doodle Engine Dev Server ${E}`)), console.log("");
15
16
  const d = {
16
17
  name: "doodle-content-loader",
17
- configureServer(t) {
18
- t.middlewares.use("/api/content", async (o, a) => {
18
+ configureServer(r) {
19
+ r.middlewares.use("/api/content", async (n, l) => {
19
20
  try {
20
- const e = await B(s);
21
- a.setHeader("Content-Type", "application/json"), a.end(JSON.stringify(e));
22
- } catch (e) {
23
- console.error(c.red(" Error loading content:"), e), a.statusCode = 500, a.end(JSON.stringify({ error: "Failed to load content" }));
21
+ const i = await x(s);
22
+ l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(i));
23
+ } catch (i) {
24
+ console.error(t.red(" Error loading content:"), i), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
24
25
  }
25
26
  });
26
- const n = D(i(s, "**/*"), {
27
+ const c = I(o(s, "**/*"), {
27
28
  ignored: /(^|[\/\\])\../,
28
29
  persistent: !0
29
30
  });
30
- n.on("change", (o) => {
31
- console.log(c.yellow(` ${S} Content changed: ${o}`)), t.ws.send({
31
+ c.on("change", (n) => {
32
+ console.log(t.yellow(` ${q} Content changed: ${n}`)), r.ws.send({
32
33
  type: "full-reload",
33
34
  path: "*"
34
35
  });
35
- }), n.on("add", (o) => {
36
- console.log(c.green(` ${j} Content added: ${o}`)), t.ws.send({
36
+ }), c.on("add", (n) => {
37
+ console.log(t.green(` ${H} Content added: ${n}`)), r.ws.send({
37
38
  type: "full-reload",
38
39
  path: "*"
39
40
  });
40
41
  });
41
42
  }
42
- }, f = await k({
43
- root: r,
44
- plugins: [b(), d],
43
+ }, h = await T({
44
+ root: e,
45
+ plugins: [O(), d],
45
46
  server: {
46
47
  port: 3e3,
47
48
  open: !0
48
49
  }
49
50
  });
50
- await f.listen(), f.printUrls(), console.log(""), console.log(c.dim(` ${E} Watching content files for changes...`)), console.log("");
51
+ await h.listen(), h.printUrls(), console.log(""), console.log(t.dim(` ${G} Watching content files for changes...`)), console.log("");
51
52
  }
52
- async function B(r) {
53
+ async function x(e) {
53
54
  const s = {
54
55
  locations: {},
55
56
  characters: {},
@@ -61,7 +62,7 @@ async function B(r) {
61
62
  locales: {}
62
63
  };
63
64
  let d = null;
64
- const f = [
65
+ const h = [
65
66
  { dir: "locations", key: "locations" },
66
67
  { dir: "characters", key: "characters" },
67
68
  { dir: "items", key: "items" },
@@ -69,41 +70,41 @@ async function B(r) {
69
70
  { dir: "quests", key: "quests" },
70
71
  { dir: "journal", key: "journalEntries" }
71
72
  ];
72
- for (const { dir: t, key: n } of f) {
73
- const o = i(r, t);
73
+ for (const { dir: r, key: c } of h) {
74
+ const n = o(e, r);
74
75
  try {
75
- const a = await w(o);
76
- for (const e of a)
77
- if (u(e) === ".yaml" || u(e) === ".yml") {
78
- const m = i(o, e), l = await p(m, "utf-8"), g = h(l);
79
- g && g.id && (s[n][g.id] = g);
76
+ const l = await _(n);
77
+ for (const i of l)
78
+ if (g(i) === ".yaml" || g(i) === ".yml") {
79
+ const u = o(n, i), m = await y(u, "utf-8"), b = w(m);
80
+ b && b.id && (s[c][b.id] = b);
80
81
  }
81
82
  } catch {
82
83
  }
83
84
  }
84
85
  try {
85
- const t = i(r, "locales"), n = await w(t);
86
- for (const o of n)
87
- if (u(o) === ".yaml" || u(o) === ".yml") {
88
- const a = i(t, o), e = await p(a, "utf-8"), m = h(e), l = o.replace(/\.(yaml|yml)$/, "");
89
- s.locales[l] = m ?? {};
86
+ const r = o(e, "locales"), c = await _(r);
87
+ for (const n of c)
88
+ if (g(n) === ".yaml" || g(n) === ".yml") {
89
+ const l = o(r, n), i = await y(l, "utf-8"), u = w(i), m = n.replace(/\.(yaml|yml)$/, "");
90
+ s.locales[m] = u ?? {};
90
91
  }
91
92
  } catch {
92
93
  }
93
94
  try {
94
- const t = i(r, "dialogues"), n = await w(t);
95
- for (const o of n)
96
- if (u(o) === ".dlg") {
97
- const a = i(t, o), e = await p(a, "utf-8"), m = o.replace(".dlg", ""), l = P(e, m);
98
- s.dialogues[l.id] = l;
95
+ const r = o(e, "dialogues"), c = await _(r);
96
+ for (const n of c)
97
+ if (g(n) === ".dlg") {
98
+ const l = o(r, n), i = await y(l, "utf-8"), u = n.replace(".dlg", ""), m = R(i, u);
99
+ s.dialogues[m.id] = m;
99
100
  }
100
101
  } catch {
101
102
  }
102
103
  try {
103
- const t = i(r, "game.yaml"), n = await p(t, "utf-8");
104
- d = h(n);
104
+ const r = o(e, "game.yaml"), c = await y(r, "utf-8");
105
+ d = w(c);
105
106
  } catch {
106
- console.warn(c.yellow(" No game.yaml found, using defaults")), d = {
107
+ console.warn(t.yellow(" No game.yaml found, using defaults")), d = {
107
108
  startLocation: "tavern",
108
109
  startTime: { day: 1, hour: 8 },
109
110
  startFlags: {},
@@ -113,14 +114,14 @@ async function B(r) {
113
114
  }
114
115
  return { registry: s, config: d };
115
116
  }
116
- async function N() {
117
- const r = process.cwd();
117
+ async function M() {
118
+ const e = process.cwd();
118
119
  console.log(`🐕🎮 Building Doodle Engine game...
119
120
  `);
120
121
  try {
121
- await C({
122
- root: r,
123
- plugins: [b()],
122
+ await N({
123
+ root: e,
124
+ plugins: [O()],
124
125
  build: {
125
126
  outDir: "dist",
126
127
  emptyOutDir: !0
@@ -133,12 +134,720 @@ async function N() {
133
134
  console.error("Build failed:", s), process.exit(1);
134
135
  }
135
136
  }
136
- const y = new $();
137
- y.name("doodle").description(c.magenta("🐾 Doodle Engine") + c.dim(" — Narrative RPG development tools")).version("0.0.1");
138
- y.command("dev").description("Start development server with hot reload").action(async () => {
139
- await q();
137
+ const k = "🐾", $ = "🐕", v = "🦴", B = "✨", D = "📁", f = "✅", F = "🚀";
138
+ async function U(e) {
139
+ const s = o(process.cwd(), e);
140
+ console.log(""), console.log(t.bold.magenta(` ${k} Doodle Engine ${k}`)), console.log(t.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${$} Creating new game: ${t.bold.cyan(e)}`), console.log("");
141
+ const { useDefaultRenderer: d } = await A({
142
+ type: "confirm",
143
+ name: "useDefaultRenderer",
144
+ message: "Use default renderer?",
145
+ initial: !0
146
+ });
147
+ d === void 0 && (console.log(t.yellow(`
148
+ ${v} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await J(s, e, d), console.log(""), console.log(t.bold.green(` ${f} Project created successfully!`)), console.log(""), console.log(t.dim(` ${D} ${s}`)), console.log(""), console.log(t.bold(" Next steps:")), console.log(t.cyan(` cd ${e}`)), console.log(t.cyan(" npm install ") + t.dim("# or: yarn install / pnpm install")), console.log(t.cyan(" npm run dev ") + t.dim("# or: yarn dev / pnpm dev")), console.log(""), console.log(t.dim(` ${F} Happy game making! ${k}`)), console.log("");
149
+ }
150
+ async function J(e, s, d) {
151
+ const h = [
152
+ "content/locations",
153
+ "content/characters",
154
+ "content/items",
155
+ "content/dialogues",
156
+ "content/quests",
157
+ "content/journal",
158
+ "content/locales",
159
+ "content/maps",
160
+ "assets/images/banners",
161
+ "assets/images/portraits",
162
+ "assets/images/items",
163
+ "assets/images/maps",
164
+ "assets/audio/music",
165
+ "assets/audio/sfx",
166
+ "assets/audio/voice",
167
+ "src"
168
+ ];
169
+ console.log(` ${D} ${t.bold("Creating directories...")}`);
170
+ for (const m of h)
171
+ await S(o(e, m), { recursive: !0 });
172
+ console.log(t.green(` ${f} Directories created`)), console.log("");
173
+ const r = {
174
+ name: s,
175
+ version: "0.1.0",
176
+ type: "module",
177
+ scripts: {
178
+ dev: "doodle dev",
179
+ build: "doodle build",
180
+ preview: "vite preview"
181
+ },
182
+ dependencies: {
183
+ "@doodle-engine/core": "latest",
184
+ "@doodle-engine/react": "latest",
185
+ react: "^19.0.0",
186
+ "react-dom": "^19.0.0"
187
+ },
188
+ devDependencies: {
189
+ "@doodle-engine/cli": "latest",
190
+ "@types/react": "^19.0.0",
191
+ "@types/react-dom": "^19.0.0",
192
+ "@vitejs/plugin-react": "^4.3.0",
193
+ typescript: "^5.7.0",
194
+ vite: "^6.0.0"
195
+ }
196
+ };
197
+ console.log(` ${B} ${t.bold("Writing project files...")}`), await a(
198
+ o(e, "package.json"),
199
+ JSON.stringify(r, null, 2)
200
+ );
201
+ const c = {
202
+ compilerOptions: {
203
+ target: "ES2024",
204
+ lib: ["ES2024", "DOM", "DOM.Iterable"],
205
+ module: "ESNext",
206
+ moduleResolution: "bundler",
207
+ jsx: "react-jsx",
208
+ strict: !0,
209
+ skipLibCheck: !0,
210
+ esModuleInterop: !0,
211
+ forceConsistentCasingInFileNames: !0,
212
+ resolveJsonModule: !0,
213
+ isolatedModules: !0,
214
+ noEmit: !0
215
+ },
216
+ include: ["src"]
217
+ };
218
+ await a(
219
+ o(e, "tsconfig.json"),
220
+ JSON.stringify(c, null, 2)
221
+ ), await a(o(e, "index.html"), `<!doctype html>
222
+ <html lang="en">
223
+ <head>
224
+ <meta charset="UTF-8" />
225
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
226
+ <title>Doodle Engine Game</title>
227
+ </head>
228
+ <body>
229
+ <div id="root"></div>
230
+ <script type="module" src="/src/main.tsx"><\/script>
231
+ </body>
232
+ </html>
233
+ `), await a(o(e, "src/main.tsx"), `import { StrictMode } from 'react'
234
+ import { createRoot } from 'react-dom/client'
235
+ import { App } from './App'
236
+ import './index.css'
237
+
238
+ createRoot(document.getElementById('root')!).render(
239
+ <StrictMode>
240
+ <App />
241
+ </StrictMode>,
242
+ )
243
+ `);
244
+ let i;
245
+ d ? i = `import { useEffect, useState } from 'react'
246
+ import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
247
+ import { GameShell } from '@doodle-engine/react'
248
+
249
+ export function App() {
250
+ const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
251
+
252
+ useEffect(() => {
253
+ fetch('/api/content')
254
+ .then(res => res.json())
255
+ .then(data => setContent({ registry: data.registry, config: data.config }))
256
+ }, [])
257
+
258
+ if (!content) {
259
+ return <div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>Loading game...</div>
260
+ }
261
+
262
+ return (
263
+ <GameShell
264
+ registry={content.registry}
265
+ config={content.config}
266
+ title="My Doodle Game"
267
+ subtitle="A text-based adventure"
268
+ splashDuration={2000}
269
+ availableLocales={[{ code: 'en', label: 'English' }]}
270
+ />
271
+ )
272
+ }
273
+ ` : i = `import { useEffect, useState } from 'react'
274
+ import { Engine } from '@doodle-engine/core'
275
+ import type { GameState, Snapshot } from '@doodle-engine/core'
276
+ import { GameProvider, useGame } from '@doodle-engine/react'
277
+
278
+ export function App() {
279
+ const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
280
+
281
+ useEffect(() => {
282
+ fetch('/api/content')
283
+ .then(res => res.json())
284
+ .then(data => {
285
+ const engine = new Engine(data.registry, createEmptyState())
286
+ const snapshot = engine.newGame(data.config)
287
+ setGame({ engine, snapshot })
288
+ })
289
+ }, [])
290
+
291
+ if (!game) {
292
+ return <div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>Loading game...</div>
293
+ }
294
+
295
+ return (
296
+ <GameProvider engine={game.engine} initialSnapshot={game.snapshot}>
297
+ <GameUI />
298
+ </GameProvider>
299
+ )
300
+ }
301
+
302
+ function GameUI() {
303
+ const { snapshot, actions } = useGame()
304
+
305
+ return (
306
+ <div style={{ padding: '2rem', fontFamily: 'sans-serif', maxWidth: '800px', margin: '0 auto' }}>
307
+ <h1>{snapshot.location.name}</h1>
308
+ <p>{snapshot.location.description}</p>
309
+
310
+ {snapshot.dialogue && (
311
+ <div style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px', margin: '1rem 0' }}>
312
+ <strong>{snapshot.dialogue.speakerName}:</strong>
313
+ <p>{snapshot.dialogue.text}</p>
314
+ {snapshot.choices.map(choice => (
315
+ <button
316
+ key={choice.id}
317
+ onClick={() => actions.selectChoice(choice.id)}
318
+ style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
319
+ >
320
+ {choice.text}
321
+ </button>
322
+ ))}
323
+ </div>
324
+ )}
325
+
326
+ {!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
327
+ <div>
328
+ <h2>Characters here</h2>
329
+ {snapshot.charactersHere.map(char => (
330
+ <button
331
+ key={char.id}
332
+ onClick={() => actions.talkTo(char.id)}
333
+ style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
334
+ >
335
+ Talk to {char.name}
336
+ </button>
337
+ ))}
338
+ </div>
339
+ )}
340
+ </div>
341
+ )
342
+ }
343
+
344
+ function createEmptyState(): GameState {
345
+ return {
346
+ currentLocation: '',
347
+ currentTime: { day: 1, hour: 0 },
348
+ flags: {},
349
+ variables: {},
350
+ inventory: [],
351
+ questProgress: {},
352
+ unlockedJournalEntries: [],
353
+ playerNotes: [],
354
+ dialogueState: null,
355
+ characterState: {},
356
+ itemLocations: {},
357
+ mapEnabled: true,
358
+ notifications: [],
359
+ pendingSounds: [],
360
+ pendingVideo: null,
361
+ currentLocale: 'en',
362
+ }
363
+ }
364
+ `, await a(o(e, "src/App.tsx"), i), await a(o(e, "src/index.css"), `body {
365
+ margin: 0;
366
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
367
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
368
+ sans-serif;
369
+ -webkit-font-smoothing: antialiased;
370
+ -moz-osx-font-smoothing: grayscale;
371
+ }
372
+ `), console.log(t.green(` ${f} Source files created`)), console.log(""), console.log(` ${v} ${t.bold("Writing starter content...")}`), await a(o(e, "content/game.yaml"), `# Game Configuration
373
+ startLocation: tavern
374
+ startTime:
375
+ day: 1
376
+ hour: 8
377
+ startFlags: {}
378
+ startVariables:
379
+ gold: 100
380
+ reputation: 0
381
+ _drinksBought: 0
382
+ startInventory: []
383
+ `), await a(o(e, "content/locations/tavern.yaml"), `id: tavern
384
+ name: "@location.tavern.name"
385
+ description: "@location.tavern.description"
386
+ banner: ""
387
+ music: ""
388
+ ambient: ""
389
+ `), await a(o(e, "content/locations/market.yaml"), `id: market
390
+ name: "@location.market.name"
391
+ description: "@location.market.description"
392
+ banner: ""
393
+ music: ""
394
+ ambient: ""
395
+ `), await a(o(e, "content/characters/bartender.yaml"), `id: bartender
396
+ name: "@character.bartender.name"
397
+ biography: "@character.bartender.bio"
398
+ portrait: ""
399
+ location: tavern
400
+ dialogue: bartender_greeting
401
+ stats: {}
402
+ `), await a(o(e, "content/characters/merchant.yaml"), `id: merchant
403
+ name: "@character.merchant.name"
404
+ biography: "@character.merchant.bio"
405
+ portrait: ""
406
+ location: market
407
+ dialogue: merchant_intro
408
+ stats: {}
409
+ `), await a(o(e, "content/items/old_coin.yaml"), `id: old_coin
410
+ name: "@item.old_coin.name"
411
+ description: "@item.old_coin.description"
412
+ icon: ""
413
+ image: ""
414
+ location: tavern
415
+ stats: {}
416
+ `), await a(o(e, "content/maps/town.yaml"), `id: town
417
+ name: "@map.town.name"
418
+ image: ""
419
+ scale: 1
420
+ locations:
421
+ - id: tavern
422
+ x: 100
423
+ y: 200
424
+ - id: market
425
+ x: 300
426
+ y: 150
427
+ `), await a(o(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
428
+ name: "@quest.odd_jobs.name"
429
+ description: "@quest.odd_jobs.description"
430
+ stages:
431
+ - id: started
432
+ description: "@quest.odd_jobs.stage.started"
433
+ - id: talked_to_merchant
434
+ description: "@quest.odd_jobs.stage.talked_to_merchant"
435
+ - id: complete
436
+ description: "@quest.odd_jobs.stage.complete"
437
+ `), await a(o(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
438
+ title: "@journal.tavern_discovery.title"
439
+ text: "@journal.tavern_discovery.text"
440
+ category: places
441
+ `), await a(o(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
442
+ title: "@journal.odd_jobs_accepted.title"
443
+ text: "@journal.odd_jobs_accepted.text"
444
+ category: quests
445
+ `), await a(o(e, "content/journal/market_square.yaml"), `id: market_square
446
+ title: "@journal.market_square.title"
447
+ text: "@journal.market_square.text"
448
+ category: places
449
+ `), await a(o(e, "content/dialogues/tavern_intro.dlg"), `TRIGGER tavern
450
+ REQUIRE notFlag seenTavernIntro
451
+
452
+ NODE start
453
+ NARRATOR: @narrator.tavern_intro
454
+ SET flag seenTavernIntro
455
+
456
+ CHOICE @narrator.choice.look_around
457
+ END dialogue
458
+ END
459
+ `), await a(o(e, "content/dialogues/market_intro.dlg"), `TRIGGER market
460
+ REQUIRE notFlag seenMarketIntro
461
+
462
+ NODE start
463
+ NARRATOR: @narrator.market_intro
464
+ SET flag seenMarketIntro
465
+ ADD journalEntry market_square
466
+
467
+ CHOICE @narrator.choice.look_around
468
+ END dialogue
469
+ END
470
+ `), await a(o(e, "content/dialogues/bartender_greeting.dlg"), `NODE start
471
+ BARTENDER: @bartender.greeting
472
+
473
+ # Always available — ask for rumors (demonstrates: flag, relationship, journalEntry)
474
+ CHOICE @bartender.choice.whats_news
475
+ SET flag metBartender
476
+ ADD relationship bartender 1
477
+ GOTO rumors
478
+ END
479
+
480
+ # Always available — buy a drink (demonstrates: variable change, flag)
481
+ CHOICE @bartender.choice.order_drink
482
+ GOTO order_drink
483
+ END
484
+
485
+ # Only before accepting the quest (demonstrates: notFlag condition)
486
+ CHOICE @bartender.choice.looking_for_work
487
+ REQUIRE notFlag acceptedOddJobs
488
+ GOTO work_intro
489
+ END
490
+
491
+ # Only while quest is active at "started" stage (demonstrates: questAtStage condition)
492
+ CHOICE @bartender.choice.about_that_job
493
+ REQUIRE questAtStage odd_jobs started
494
+ GOTO work_followup
495
+ END
496
+
497
+ # Only after quest is complete (demonstrates: questAtStage condition)
498
+ CHOICE @bartender.choice.thanks_for_work
499
+ REQUIRE questAtStage odd_jobs complete
500
+ GOTO work_done
501
+ END
502
+
503
+ CHOICE @bartender.choice.nevermind
504
+ GOTO farewell
505
+ END
506
+
507
+ NODE rumors
508
+ BARTENDER: @bartender.rumors
509
+ ADD journalEntry tavern_discovery
510
+ ADD item old_coin
511
+ NOTIFY @notification.journal_updated
512
+
513
+ CHOICE @bartender.choice.tell_me_more
514
+ GOTO rumors_detail
515
+ END
516
+
517
+ CHOICE @bartender.choice.back_to_chat
518
+ GOTO start
519
+ END
520
+
521
+ NODE rumors_detail
522
+ BARTENDER: @bartender.rumors_detail
523
+
524
+ CHOICE @bartender.choice.interesting
525
+ GOTO start
526
+ END
527
+
528
+ NODE order_drink
529
+ BARTENDER: @bartender.order_drink
530
+
531
+ # Only if player can afford it (demonstrates: variableGreaterThan condition)
532
+ CHOICE @bartender.choice.sure_pay
533
+ REQUIRE variableGreaterThan gold 4
534
+ ADD variable gold -5
535
+ ADD variable _drinksBought 1
536
+ ADD variable reputation 1
537
+ SET flag hadDrink
538
+ ADD relationship bartender 1
539
+ NOTIFY @notification.bought_drink
540
+ GOTO after_drink
541
+ END
542
+
543
+ # Always available as an out
544
+ CHOICE @bartender.choice.too_rich
545
+ GOTO start
546
+ END
547
+
548
+ NODE after_drink
549
+ BARTENDER: @bartender.after_drink
550
+
551
+ CHOICE @bartender.choice.back_to_chat
552
+ GOTO start
553
+ END
554
+
555
+ NODE work_intro
556
+ BARTENDER: @bartender.work_intro
557
+
558
+ CHOICE @bartender.choice.accept_work
559
+ SET flag acceptedOddJobs
560
+ SET questStage odd_jobs started
561
+ ADD journalEntry odd_jobs_accepted
562
+ ADD relationship bartender 2
563
+ ADD variable reputation 5
564
+ NOTIFY @notification.quest_started
565
+ GOTO work_accepted
566
+ END
567
+
568
+ CHOICE @bartender.choice.not_interested
569
+ GOTO start
570
+ END
571
+
572
+ NODE work_accepted
573
+ BARTENDER: @bartender.work_accepted
574
+
575
+ CHOICE @bartender.choice.on_my_way
576
+ GOTO farewell
577
+ END
578
+
579
+ CHOICE @bartender.choice.more_details
580
+ GOTO work_details
581
+ END
582
+
583
+ NODE work_details
584
+ BARTENDER: @bartender.work_details
585
+
586
+ CHOICE @bartender.choice.got_it
587
+ GOTO farewell
588
+ END
589
+
590
+ NODE work_followup
591
+ BARTENDER: @bartender.work_followup
592
+
593
+ CHOICE @bartender.choice.on_my_way
594
+ GOTO farewell
595
+ END
596
+
597
+ NODE work_done
598
+ BARTENDER: @bartender.work_done
599
+ ADD relationship bartender 3
600
+
601
+ CHOICE @bartender.choice.anytime
602
+ GOTO start
603
+ END
604
+
605
+ NODE farewell
606
+ BARTENDER: @bartender.farewell
607
+ END dialogue
608
+ `), await a(o(e, "content/dialogues/merchant_intro.dlg"), `NODE start
609
+ MERCHANT: @merchant.greeting
610
+
611
+ CHOICE @merchant.choice.browse_wares
612
+ GOTO browse
613
+ END
614
+
615
+ # Only appears when quest is at "started" (bartender sent you)
616
+ CHOICE @merchant.choice.heard_about_work
617
+ REQUIRE questAtStage odd_jobs started
618
+ GOTO odd_jobs_talk
619
+ END
620
+
621
+ # Only appears after talking to merchant about the job
622
+ CHOICE @merchant.choice.delivery_done
623
+ REQUIRE questAtStage odd_jobs talked_to_merchant
624
+ GOTO quest_complete
625
+ END
626
+
627
+ CHOICE @merchant.choice.whats_this_place
628
+ GOTO about_market
629
+ END
630
+
631
+ CHOICE @merchant.choice.goodbye
632
+ GOTO farewell
633
+ END
634
+
635
+ NODE browse
636
+ MERCHANT: @merchant.browse
637
+
638
+ # Conditional purchase — need enough gold (demonstrates: variableGreaterThan)
639
+ CHOICE @merchant.choice.buy_map
640
+ REQUIRE variableGreaterThan gold 19
641
+ ADD variable gold -20
642
+ SET flag boughtMap
643
+ NOTIFY @notification.bought_map
644
+ GOTO sold_map
645
+ END
646
+
647
+ CHOICE @merchant.choice.too_pricey
648
+ GOTO too_pricey
649
+ END
650
+
651
+ NODE sold_map
652
+ MERCHANT: @merchant.sold_map
653
+
654
+ CHOICE @merchant.choice.thanks_info
655
+ GOTO start
656
+ END
657
+
658
+ NODE too_pricey
659
+ MERCHANT: @merchant.too_pricey
660
+
661
+ CHOICE @merchant.choice.back_to_browse
662
+ GOTO start
663
+ END
664
+
665
+ NODE odd_jobs_talk
666
+ MERCHANT: @merchant.odd_jobs
667
+
668
+ CHOICE @merchant.choice.accept_task
669
+ SET questStage odd_jobs talked_to_merchant
670
+ ADD relationship merchant 1
671
+ NOTIFY @notification.quest_updated
672
+ GOTO task_details
673
+ END
674
+
675
+ CHOICE @merchant.choice.need_to_think
676
+ GOTO farewell
677
+ END
678
+
679
+ NODE task_details
680
+ MERCHANT: @merchant.task_details
681
+
682
+ CHOICE @merchant.choice.on_it
683
+ GOTO farewell
684
+ END
685
+
686
+ NODE quest_complete
687
+ MERCHANT: @merchant.quest_complete
688
+ SET questStage odd_jobs complete
689
+ ADD variable gold 50
690
+ ADD variable reputation 10
691
+ ADD relationship merchant 3
692
+ NOTIFY @notification.quest_complete
693
+
694
+ CHOICE @merchant.choice.glad_to_help
695
+ GOTO farewell
696
+ END
697
+
698
+ NODE about_market
699
+ MERCHANT: @merchant.about_market
700
+
701
+ CHOICE @merchant.choice.thanks_info
702
+ GOTO start
703
+ END
704
+
705
+ NODE farewell
706
+ MERCHANT: @merchant.farewell
707
+ END dialogue
708
+ `), await a(o(e, "content/locales/en.yaml"), `# ===================
709
+ # Narrator Intros
710
+ # ===================
711
+ 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."
712
+ 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."
713
+ narrator.choice.look_around: "Look around."
714
+
715
+ # ===================
716
+ # Locations
717
+ # ===================
718
+ location.tavern.name: "The Salty Dog"
719
+ location.tavern.description: "A dimly lit tavern smelling of salt and stale ale. Candles flicker on rough wooden tables, and the murmur of conversation fills the air."
720
+ location.market.name: "Market Square"
721
+ location.market.description: "A bustling open-air market where merchants hawk their wares. The smell of fresh bread mingles with exotic spices."
722
+
723
+ # ===================
724
+ # Characters
725
+ # ===================
726
+ character.bartender.name: "Marcus the Bartender"
727
+ character.bartender.bio: "A gruff man with kind eyes who's heard every story twice. He keeps the peace at The Salty Dog with a firm hand and a generous pour."
728
+ character.merchant.name: "Elena the Merchant"
729
+ character.merchant.bio: "A sharp-eyed trader who always seems to know the value of everything, and the price of everyone."
730
+
731
+ # ===================
732
+ # Items
733
+ # ===================
734
+ item.old_coin.name: "Old Coin"
735
+ item.old_coin.description: "A tarnished coin with strange markings. It doesn't match any currency you've seen before."
736
+
737
+ # ===================
738
+ # Maps
739
+ # ===================
740
+ map.town.name: "Town"
741
+
742
+ # ===================
743
+ # Quests
744
+ # ===================
745
+ quest.odd_jobs.name: "Odd Jobs"
746
+ quest.odd_jobs.description: "The bartender mentioned someone at the market who could use a hand."
747
+ quest.odd_jobs.stage.started: "Marcus mentioned work at the market. I should talk to the merchant there."
748
+ quest.odd_jobs.stage.talked_to_merchant: "Elena needs a delivery watched. Time to head to the docks."
749
+ quest.odd_jobs.stage.complete: "Job well done. Elena paid 50 gold for the trouble."
750
+
751
+ # ===================
752
+ # Journal Entries
753
+ # ===================
754
+ journal.tavern_discovery.title: "The Salty Dog"
755
+ 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."
756
+ journal.odd_jobs_accepted.title: "Work at the Market"
757
+ 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."
758
+ journal.market_square.title: "Market Square"
759
+ 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."
760
+
761
+ # ===================
762
+ # Bartender Dialogue
763
+ # ===================
764
+ bartender.greeting: "Welcome to the Salty Dog, stranger. What can I get you?"
765
+ bartender.farewell: "Take care out there. The streets aren't as safe as they used to be."
766
+
767
+ # Choices
768
+ bartender.choice.whats_news: "What's the news around here?"
769
+ bartender.choice.order_drink: "I'll have a drink."
770
+ bartender.choice.looking_for_work: "I'm looking for work."
771
+ bartender.choice.about_that_job: "About that job you mentioned..."
772
+ bartender.choice.thanks_for_work: "Thanks for putting me onto that work."
773
+ bartender.choice.nevermind: "Never mind, just passing through."
774
+ bartender.choice.tell_me_more: "Tell me more about that."
775
+ bartender.choice.interesting: "Interesting. I'll keep that in mind."
776
+ bartender.choice.sure_pay: "Sure, here's five gold."
777
+ bartender.choice.too_rich: "On second thought, I'll pass."
778
+ bartender.choice.back_to_chat: "So, what else?"
779
+ bartender.choice.accept_work: "Sure, I could use the coin."
780
+ bartender.choice.not_interested: "Not right now, thanks."
781
+ bartender.choice.on_my_way: "I'll head there now."
782
+ bartender.choice.more_details: "What exactly do they need?"
783
+ bartender.choice.got_it: "Got it. I'll take care of it."
784
+ bartender.choice.anytime: "Anytime."
785
+
786
+ # Responses
787
+ 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."
788
+ 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."
789
+ bartender.order_drink: "Five gold for the house special — strongest thing this side of the river. What do you say?"
790
+ bartender.after_drink: "Glad you like it! Brewed it myself. Now then, anything else?"
791
+ 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."
792
+ bartender.work_accepted: "Good on you. Elena's fair with pay. Head to the market square when you're ready."
793
+ bartender.work_details: "Something about a shipment that needs escorting. Nothing too dangerous, she says — but then, that's what they always say."
794
+ 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."
795
+ bartender.work_done: "I heard Elena's singing your praises. Good work out there — I knew you had it in you."
796
+
797
+ # ===================
798
+ # Merchant Dialogue
799
+ # ===================
800
+ merchant.greeting: "Welcome, welcome! Elena's Emporium has everything you need, and plenty you didn't know you wanted."
801
+ merchant.farewell: "Safe travels! Come back anytime."
802
+
803
+ # Choices
804
+ merchant.choice.browse_wares: "Let me see what you've got."
805
+ merchant.choice.heard_about_work: "Marcus sent me about some work."
806
+ merchant.choice.delivery_done: "The delivery is done."
807
+ merchant.choice.whats_this_place: "Tell me about the market."
808
+ merchant.choice.goodbye: "Just browsing. Goodbye."
809
+ merchant.choice.buy_map: "I'll take the map. (20 gold)"
810
+ merchant.choice.too_pricey: "A bit rich for my blood."
811
+ merchant.choice.accept_task: "I'm in. What do you need?"
812
+ merchant.choice.need_to_think: "Let me think about it."
813
+ merchant.choice.thanks_info: "Thanks for the info."
814
+ merchant.choice.back_to_browse: "I'll keep looking around."
815
+ merchant.choice.on_it: "Consider it done."
816
+ merchant.choice.glad_to_help: "Glad I could help."
817
+
818
+ # Responses
819
+ 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."
820
+ merchant.sold_map: "Excellent choice! This'll keep you from wandering into the wrong part of town."
821
+ 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."
822
+ 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?"
823
+ 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."
824
+ 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."
825
+ 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."
826
+
827
+ # ===================
828
+ # Notifications
829
+ # ===================
830
+ notification.journal_updated: "Journal Updated"
831
+ notification.quest_started: "New Quest: Odd Jobs"
832
+ notification.quest_updated: "Quest Updated: Odd Jobs"
833
+ notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
834
+ notification.bought_drink: "Bought a drink (-5 gold)"
835
+ notification.bought_map: "Bought a map (-20 gold)"
836
+ `), console.log(t.green(` ${f} Starter content created`)), console.log(""), console.log(t.dim(" Content includes:")), console.log(t.dim(" 2 locations (tavern, market)")), console.log(t.dim(" 2 characters (bartender, merchant)")), console.log(t.dim(" 1 item (old coin)")), console.log(t.dim(" 1 map (town)")), console.log(t.dim(" 1 quest with 3 stages")), console.log(t.dim(" 3 journal entries")), console.log(t.dim(" 4 dialogues (2 narrator intros, 2 NPC conversations)")), console.log(t.dim(" English locale with all strings")), await a(o(e, ".gitignore"), `node_modules
837
+ dist
838
+ .DS_Store
839
+ *.log
840
+ `);
841
+ }
842
+ const p = new C();
843
+ p.name("doodle").description(t.magenta("🐾 Doodle Engine") + t.dim(" — Narrative RPG development tools")).version("0.0.1");
844
+ p.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
845
+ await U(e);
846
+ });
847
+ p.command("dev").description("Start development server with hot reload").action(async () => {
848
+ await j();
140
849
  });
141
- y.command("build").description("Build game for production").action(async () => {
142
- await N();
850
+ p.command("build").description("Build game for production").action(async () => {
851
+ await M();
143
852
  });
144
- y.parse();
853
+ p.parse();