@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 +771 -62
- package/dist/create.d.ts +2 -2
- package/dist/create.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -2
- package/dist/create.js +0 -716
package/dist/cli.js
CHANGED
|
@@ -1,55 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command as
|
|
3
|
-
import { crayon as
|
|
4
|
-
import { createServer as
|
|
5
|
-
import
|
|
6
|
-
import { watch as
|
|
7
|
-
import { readdir as
|
|
8
|
-
import { join as
|
|
9
|
-
import { parse as
|
|
10
|
-
import { parseDialogue as
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
18
|
-
|
|
18
|
+
configureServer(r) {
|
|
19
|
+
r.middlewares.use("/api/content", async (n, l) => {
|
|
19
20
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
} catch (
|
|
23
|
-
console.error(
|
|
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
|
|
27
|
+
const c = I(o(s, "**/*"), {
|
|
27
28
|
ignored: /(^|[\/\\])\../,
|
|
28
29
|
persistent: !0
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
-
console.log(
|
|
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
|
-
}),
|
|
36
|
-
console.log(
|
|
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
|
-
},
|
|
43
|
-
root:
|
|
44
|
-
plugins: [
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
73
|
-
const
|
|
73
|
+
for (const { dir: r, key: c } of h) {
|
|
74
|
+
const n = o(e, r);
|
|
74
75
|
try {
|
|
75
|
-
const
|
|
76
|
-
for (const
|
|
77
|
-
if (
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
|
86
|
-
for (const
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
s.locales[
|
|
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
|
|
95
|
-
for (const
|
|
96
|
-
if (
|
|
97
|
-
const
|
|
98
|
-
s.dialogues[
|
|
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
|
|
104
|
-
d =
|
|
104
|
+
const r = o(e, "game.yaml"), c = await y(r, "utf-8");
|
|
105
|
+
d = w(c);
|
|
105
106
|
} catch {
|
|
106
|
-
console.warn(
|
|
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
|
|
117
|
-
const
|
|
117
|
+
async function M() {
|
|
118
|
+
const e = process.cwd();
|
|
118
119
|
console.log(`🐕🎮 Building Doodle Engine game...
|
|
119
120
|
`);
|
|
120
121
|
try {
|
|
121
|
-
await
|
|
122
|
-
root:
|
|
123
|
-
plugins: [
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
await
|
|
850
|
+
p.command("build").description("Build game for production").action(async () => {
|
|
851
|
+
await M();
|
|
143
852
|
});
|
|
144
|
-
|
|
853
|
+
p.parse();
|