@doodle-engine/cli 0.0.7 → 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/CHANGELOG.md +18 -0
- package/dist/cli.js +531 -125
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,57 +1,255 @@
|
|
|
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
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
import { parse as b } from "yaml";
|
|
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
|
+
}
|
|
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
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return i.push(...J(e, n)), i;
|
|
28
|
+
}
|
|
29
|
+
function F(e, n) {
|
|
30
|
+
const i = [], a = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const o of e.nodes)
|
|
32
|
+
a.has(o.id) && i.push({
|
|
33
|
+
file: n,
|
|
34
|
+
message: `Duplicate node ID "${o.id}"`,
|
|
35
|
+
suggestion: "Node IDs must be unique within a dialogue"
|
|
36
|
+
}), a.add(o.id);
|
|
37
|
+
a.has(e.startNode) || i.push({
|
|
38
|
+
file: n,
|
|
39
|
+
message: `Start node "${e.startNode}" not found`,
|
|
40
|
+
suggestion: `Add a NODE ${e.startNode} or fix the startNode reference`
|
|
41
|
+
});
|
|
42
|
+
for (const o of e.nodes)
|
|
43
|
+
i.push(...B(o, a, n));
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
function B(e, n, i) {
|
|
47
|
+
const a = [];
|
|
48
|
+
if (e.next && !n.has(e.next) && a.push({
|
|
49
|
+
file: i,
|
|
50
|
+
message: `Node "${e.id}" GOTO "${e.next}" points to non-existent node`,
|
|
51
|
+
suggestion: `Add NODE ${e.next} or fix the GOTO target`
|
|
52
|
+
}), e.conditionalNext)
|
|
53
|
+
for (const o of e.conditionalNext)
|
|
54
|
+
n.has(o.next) || a.push({
|
|
55
|
+
file: i,
|
|
56
|
+
message: `Node "${e.id}" IF block GOTO "${o.next}" points to non-existent node`,
|
|
57
|
+
suggestion: `Add NODE ${o.next} or fix the GOTO target`
|
|
58
|
+
}), a.push(...O(o.condition, e.id, i));
|
|
59
|
+
for (const o of e.choices) {
|
|
60
|
+
if (!o.effects?.some(
|
|
61
|
+
(t) => t.type === "endDialogue" || t.type === "goToLocation"
|
|
62
|
+
) && !n.has(o.next) && a.push({
|
|
63
|
+
file: i,
|
|
64
|
+
message: `Node "${e.id}" choice "${o.id}" GOTO "${o.next}" points to non-existent node`,
|
|
65
|
+
suggestion: `Add NODE ${o.next} or fix the GOTO target`
|
|
66
|
+
}), o.conditions)
|
|
67
|
+
for (const t of o.conditions)
|
|
68
|
+
a.push(...O(t, e.id, i));
|
|
69
|
+
if (o.effects)
|
|
70
|
+
for (const t of o.effects)
|
|
71
|
+
a.push(...C(t, e.id, i));
|
|
72
|
+
}
|
|
73
|
+
if (e.conditions)
|
|
74
|
+
for (const o of e.conditions)
|
|
75
|
+
a.push(...O(o, e.id, i));
|
|
76
|
+
if (e.effects)
|
|
77
|
+
for (const o of e.effects)
|
|
78
|
+
a.push(...C(o, e.id, i));
|
|
79
|
+
return a;
|
|
80
|
+
}
|
|
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 = [];
|
|
125
|
+
if (!e.type)
|
|
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;
|
|
144
|
+
}
|
|
145
|
+
function C(e, n, i) {
|
|
146
|
+
const a = [];
|
|
147
|
+
if (!e.type)
|
|
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;
|
|
161
|
+
}
|
|
162
|
+
function J(e, n) {
|
|
163
|
+
const i = [], a = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const t of Object.values(e.locales))
|
|
165
|
+
for (const l of Object.keys(t))
|
|
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,
|
|
173
|
+
message: `Localization key "${t}" not found in any locale file`,
|
|
174
|
+
suggestion: `Add "${u}: ..." to your locale files`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
for (const t of Object.values(e.locations))
|
|
179
|
+
o(t.name) && r(t.name, t.id, "location"), o(t.description) && r(t.description, t.id, "location");
|
|
180
|
+
for (const t of Object.values(e.characters))
|
|
181
|
+
o(t.name) && r(t.name, t.id, "character"), o(t.biography) && r(t.biography, t.id, "character");
|
|
182
|
+
for (const t of Object.values(e.items))
|
|
183
|
+
o(t.name) && r(t.name, t.id, "item"), o(t.description) && r(t.description, t.id, "item");
|
|
184
|
+
for (const t of Object.values(e.quests)) {
|
|
185
|
+
o(t.name) && r(t.name, t.id, "quest"), o(t.description) && r(t.description, t.id, "quest");
|
|
186
|
+
for (const l of t.stages)
|
|
187
|
+
o(l.description) && r(l.description, t.id, "quest");
|
|
188
|
+
}
|
|
189
|
+
for (const t of Object.values(e.journalEntries))
|
|
190
|
+
o(t.title) && r(t.title, t.id, "journal"), o(t.text) && r(t.text, t.id, "journal");
|
|
191
|
+
for (const t of Object.values(e.dialogues))
|
|
192
|
+
for (const l of t.nodes) {
|
|
193
|
+
o(l.text) && r(l.text, t.id, "dialogue");
|
|
194
|
+
for (const d of l.choices)
|
|
195
|
+
o(d.text) && r(d.text, t.id, "dialogue");
|
|
196
|
+
}
|
|
197
|
+
return i;
|
|
198
|
+
}
|
|
199
|
+
function T(e) {
|
|
200
|
+
if (e.length === 0) {
|
|
201
|
+
console.log(c.green("✓ No validation errors"));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(c.red(`
|
|
205
|
+
✗ Found ${e.length} validation error${e.length === 1 ? "" : "s"}:
|
|
206
|
+
`));
|
|
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();
|
|
209
|
+
}
|
|
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 = {
|
|
17
215
|
name: "doodle-content-loader",
|
|
18
|
-
configureServer(
|
|
19
|
-
|
|
216
|
+
configureServer(o) {
|
|
217
|
+
o.middlewares.use("/api/content", async (t, l) => {
|
|
20
218
|
try {
|
|
21
|
-
const
|
|
22
|
-
l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(
|
|
23
|
-
} catch (
|
|
24
|
-
console.error(
|
|
219
|
+
const d = await P(n);
|
|
220
|
+
l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(d));
|
|
221
|
+
} catch (d) {
|
|
222
|
+
console.error(c.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
|
|
25
223
|
}
|
|
26
224
|
});
|
|
27
|
-
const
|
|
225
|
+
const r = H(n, {
|
|
28
226
|
ignored: /(^|[\/\\])\../,
|
|
29
227
|
persistent: !0
|
|
30
228
|
});
|
|
31
|
-
|
|
32
|
-
console.log(
|
|
229
|
+
r.on("change", async (t) => {
|
|
230
|
+
console.log(c.yellow(` ${Q} Content changed: ${t}`)), await S(n), o.ws.send({
|
|
33
231
|
type: "full-reload",
|
|
34
232
|
path: "*"
|
|
35
233
|
});
|
|
36
|
-
}),
|
|
37
|
-
console.log(
|
|
234
|
+
}), r.on("add", async (t) => {
|
|
235
|
+
console.log(c.green(` ${V} Content added: ${t}`)), await S(n), o.ws.send({
|
|
38
236
|
type: "full-reload",
|
|
39
237
|
path: "*"
|
|
40
238
|
});
|
|
41
239
|
});
|
|
42
240
|
}
|
|
43
|
-
},
|
|
241
|
+
}, a = await j({
|
|
44
242
|
root: e,
|
|
45
|
-
plugins: [
|
|
243
|
+
plugins: [A(), i],
|
|
46
244
|
server: {
|
|
47
245
|
port: 3e3,
|
|
48
246
|
open: !0
|
|
49
247
|
}
|
|
50
248
|
});
|
|
51
|
-
await
|
|
249
|
+
await a.listen(), a.printUrls(), console.log(""), console.log(c.dim(` ${W} Watching content files for changes...`)), console.log("");
|
|
52
250
|
}
|
|
53
|
-
async function
|
|
54
|
-
const
|
|
251
|
+
async function P(e) {
|
|
252
|
+
const n = {
|
|
55
253
|
locations: {},
|
|
56
254
|
characters: {},
|
|
57
255
|
items: {},
|
|
@@ -61,8 +259,8 @@ async function x(e) {
|
|
|
61
259
|
journalEntries: {},
|
|
62
260
|
locales: {}
|
|
63
261
|
};
|
|
64
|
-
let
|
|
65
|
-
const
|
|
262
|
+
let i = null;
|
|
263
|
+
const a = [
|
|
66
264
|
{ dir: "locations", key: "locations" },
|
|
67
265
|
{ dir: "characters", key: "characters" },
|
|
68
266
|
{ dir: "items", key: "items" },
|
|
@@ -70,41 +268,41 @@ async function x(e) {
|
|
|
70
268
|
{ dir: "quests", key: "quests" },
|
|
71
269
|
{ dir: "journal", key: "journalEntries" }
|
|
72
270
|
];
|
|
73
|
-
for (const { dir:
|
|
74
|
-
const
|
|
271
|
+
for (const { dir: o, key: r } of a) {
|
|
272
|
+
const t = s(e, o);
|
|
75
273
|
try {
|
|
76
|
-
const l = await
|
|
77
|
-
for (const
|
|
78
|
-
if (
|
|
79
|
-
const u =
|
|
80
|
-
|
|
274
|
+
const l = await y(t);
|
|
275
|
+
for (const d of l)
|
|
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);
|
|
81
279
|
}
|
|
82
280
|
} catch {
|
|
83
281
|
}
|
|
84
282
|
}
|
|
85
283
|
try {
|
|
86
|
-
const
|
|
87
|
-
for (const
|
|
88
|
-
if (
|
|
89
|
-
const l = o
|
|
90
|
-
|
|
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 ?? {};
|
|
91
289
|
}
|
|
92
290
|
} catch {
|
|
93
291
|
}
|
|
94
292
|
try {
|
|
95
|
-
const
|
|
96
|
-
for (const
|
|
97
|
-
if (
|
|
98
|
-
const l = o
|
|
99
|
-
|
|
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;
|
|
100
298
|
}
|
|
101
299
|
} catch {
|
|
102
300
|
}
|
|
103
301
|
try {
|
|
104
|
-
const
|
|
105
|
-
|
|
302
|
+
const o = s(e, "game.yaml"), r = await p(o, "utf-8");
|
|
303
|
+
i = b(r);
|
|
106
304
|
} catch {
|
|
107
|
-
console.warn(
|
|
305
|
+
console.warn(c.yellow(" No game.yaml found, using defaults")), i = {
|
|
108
306
|
startLocation: "tavern",
|
|
109
307
|
startTime: { day: 1, hour: 8 },
|
|
110
308
|
startFlags: {},
|
|
@@ -112,43 +310,224 @@ async function x(e) {
|
|
|
112
310
|
startInventory: []
|
|
113
311
|
};
|
|
114
312
|
}
|
|
115
|
-
return { registry:
|
|
313
|
+
return { registry: n, config: i };
|
|
116
314
|
}
|
|
117
|
-
async function
|
|
118
|
-
const e = process.cwd();
|
|
119
|
-
console.log(`🐕🎮 Building Doodle Engine game...
|
|
120
|
-
`);
|
|
315
|
+
async function S(e) {
|
|
121
316
|
try {
|
|
122
|
-
await
|
|
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);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function K(e) {
|
|
324
|
+
const n = {
|
|
325
|
+
locations: {},
|
|
326
|
+
characters: {},
|
|
327
|
+
items: {},
|
|
328
|
+
maps: {},
|
|
329
|
+
dialogues: {},
|
|
330
|
+
quests: {},
|
|
331
|
+
journalEntries: {},
|
|
332
|
+
locales: {}
|
|
333
|
+
}, i = /* @__PURE__ */ new Map(), a = [
|
|
334
|
+
{ dir: "locations", key: "locations" },
|
|
335
|
+
{ dir: "characters", key: "characters" },
|
|
336
|
+
{ dir: "items", key: "items" },
|
|
337
|
+
{ dir: "maps", key: "maps" },
|
|
338
|
+
{ dir: "quests", key: "quests" },
|
|
339
|
+
{ dir: "journal", key: "journalEntries" }
|
|
340
|
+
];
|
|
341
|
+
for (const { dir: o, key: r } of a) {
|
|
342
|
+
const t = s(e, o);
|
|
343
|
+
try {
|
|
344
|
+
const l = await y(t);
|
|
345
|
+
for (const d of l)
|
|
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)));
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
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 ?? {};
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
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));
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
return { registry: n, fileMap: i };
|
|
372
|
+
}
|
|
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;
|
|
377
|
+
try {
|
|
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 };
|
|
380
|
+
} catch (a) {
|
|
381
|
+
console.error(c.red("Error loading content:"), a), process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
console.log("");
|
|
384
|
+
try {
|
|
385
|
+
await $({
|
|
123
386
|
root: e,
|
|
124
|
-
plugins: [
|
|
387
|
+
plugins: [A()],
|
|
125
388
|
build: {
|
|
126
389
|
outDir: "dist",
|
|
127
390
|
emptyOutDir: !0
|
|
128
391
|
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
console.error("Build failed:", s), process.exit(1);
|
|
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("");
|
|
395
|
+
} catch (a) {
|
|
396
|
+
console.error(c.red("Build failed:"), a), process.exit(1);
|
|
135
397
|
}
|
|
136
398
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
399
|
+
async function X(e) {
|
|
400
|
+
const n = {
|
|
401
|
+
locations: {},
|
|
402
|
+
characters: {},
|
|
403
|
+
items: {},
|
|
404
|
+
maps: {},
|
|
405
|
+
dialogues: {},
|
|
406
|
+
quests: {},
|
|
407
|
+
journalEntries: {},
|
|
408
|
+
locales: {}
|
|
409
|
+
}, i = /* @__PURE__ */ new Map(), a = [
|
|
410
|
+
{ dir: "locations", key: "locations" },
|
|
411
|
+
{ dir: "characters", key: "characters" },
|
|
412
|
+
{ dir: "items", key: "items" },
|
|
413
|
+
{ dir: "maps", key: "maps" },
|
|
414
|
+
{ dir: "quests", key: "quests" },
|
|
415
|
+
{ dir: "journal", key: "journalEntries" }
|
|
416
|
+
];
|
|
417
|
+
for (const { dir: r, key: t } of a) {
|
|
418
|
+
const l = s(e, r);
|
|
419
|
+
try {
|
|
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)));
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
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 ?? {};
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
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));
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
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 };
|
|
455
|
+
}
|
|
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("");
|
|
459
|
+
try {
|
|
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);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function ee(e) {
|
|
467
|
+
const n = {
|
|
468
|
+
locations: {},
|
|
469
|
+
characters: {},
|
|
470
|
+
items: {},
|
|
471
|
+
maps: {},
|
|
472
|
+
dialogues: {},
|
|
473
|
+
quests: {},
|
|
474
|
+
journalEntries: {},
|
|
475
|
+
locales: {}
|
|
476
|
+
}, i = /* @__PURE__ */ new Map(), a = [
|
|
477
|
+
{ dir: "locations", key: "locations" },
|
|
478
|
+
{ dir: "characters", key: "characters" },
|
|
479
|
+
{ dir: "items", key: "items" },
|
|
480
|
+
{ dir: "maps", key: "maps" },
|
|
481
|
+
{ dir: "quests", key: "quests" },
|
|
482
|
+
{ dir: "journal", key: "journalEntries" }
|
|
483
|
+
];
|
|
484
|
+
for (const { dir: o, key: r } of a) {
|
|
485
|
+
const t = s(e, o);
|
|
486
|
+
try {
|
|
487
|
+
const l = await y(t);
|
|
488
|
+
for (const d of l)
|
|
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)));
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
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 ?? {};
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
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));
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
return { registry: n, fileMap: i };
|
|
515
|
+
}
|
|
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({
|
|
142
521
|
type: "confirm",
|
|
143
522
|
name: "useDefaultRenderer",
|
|
144
523
|
message: "Use default renderer?",
|
|
145
524
|
initial: !0
|
|
146
525
|
});
|
|
147
|
-
|
|
148
|
-
${
|
|
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("");
|
|
149
528
|
}
|
|
150
|
-
async function
|
|
151
|
-
const
|
|
529
|
+
async function re(e, n, i) {
|
|
530
|
+
const a = [
|
|
152
531
|
"content/locations",
|
|
153
532
|
"content/characters",
|
|
154
533
|
"content/items",
|
|
@@ -166,12 +545,12 @@ async function J(e, s, d) {
|
|
|
166
545
|
"assets/audio/voice",
|
|
167
546
|
"src"
|
|
168
547
|
];
|
|
169
|
-
console.log(` ${
|
|
170
|
-
for (const
|
|
171
|
-
await
|
|
172
|
-
console.log(
|
|
173
|
-
const
|
|
174
|
-
name:
|
|
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,
|
|
175
554
|
version: "0.1.0",
|
|
176
555
|
type: "module",
|
|
177
556
|
scripts: {
|
|
@@ -194,11 +573,11 @@ async function J(e, s, d) {
|
|
|
194
573
|
vite: "^6.0.0"
|
|
195
574
|
}
|
|
196
575
|
};
|
|
197
|
-
console.log(` ${
|
|
198
|
-
|
|
199
|
-
JSON.stringify(
|
|
576
|
+
console.log(` ${oe} ${c.bold("Writing project files...")}`), await g(
|
|
577
|
+
s(e, "package.json"),
|
|
578
|
+
JSON.stringify(o, null, 2)
|
|
200
579
|
);
|
|
201
|
-
const
|
|
580
|
+
const r = {
|
|
202
581
|
compilerOptions: {
|
|
203
582
|
target: "ES2024",
|
|
204
583
|
lib: ["ES2024", "DOM", "DOM.Iterable"],
|
|
@@ -215,10 +594,10 @@ async function J(e, s, d) {
|
|
|
215
594
|
},
|
|
216
595
|
include: ["src"]
|
|
217
596
|
};
|
|
218
|
-
await
|
|
219
|
-
|
|
220
|
-
JSON.stringify(
|
|
221
|
-
), await
|
|
597
|
+
await g(
|
|
598
|
+
s(e, "tsconfig.json"),
|
|
599
|
+
JSON.stringify(r, null, 2)
|
|
600
|
+
), await g(s(e, "index.html"), `<!doctype html>
|
|
222
601
|
<html lang="en">
|
|
223
602
|
<head>
|
|
224
603
|
<meta charset="UTF-8" />
|
|
@@ -230,7 +609,7 @@ async function J(e, s, d) {
|
|
|
230
609
|
<script type="module" src="/src/main.tsx"><\/script>
|
|
231
610
|
</body>
|
|
232
611
|
</html>
|
|
233
|
-
`), await
|
|
612
|
+
`), await g(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
|
|
234
613
|
import { createRoot } from 'react-dom/client'
|
|
235
614
|
import { App } from './App'
|
|
236
615
|
import './index.css'
|
|
@@ -241,10 +620,10 @@ createRoot(document.getElementById('root')!).render(
|
|
|
241
620
|
</StrictMode>,
|
|
242
621
|
)
|
|
243
622
|
`);
|
|
244
|
-
let
|
|
245
|
-
|
|
623
|
+
let d;
|
|
624
|
+
i ? d = `import { useEffect, useState } from 'react'
|
|
246
625
|
import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
|
|
247
|
-
import { GameShell } from '@doodle-engine/react'
|
|
626
|
+
import { GameShell, LoadingScreen } from '@doodle-engine/react'
|
|
248
627
|
|
|
249
628
|
export function App() {
|
|
250
629
|
const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
|
|
@@ -256,7 +635,7 @@ export function App() {
|
|
|
256
635
|
}, [])
|
|
257
636
|
|
|
258
637
|
if (!content) {
|
|
259
|
-
return <
|
|
638
|
+
return <LoadingScreen />
|
|
260
639
|
}
|
|
261
640
|
|
|
262
641
|
return (
|
|
@@ -267,13 +646,14 @@ export function App() {
|
|
|
267
646
|
subtitle="A text-based adventure"
|
|
268
647
|
splashDuration={2000}
|
|
269
648
|
availableLocales={[{ code: 'en', label: 'English' }]}
|
|
649
|
+
devTools={import.meta.env.DEV}
|
|
270
650
|
/>
|
|
271
651
|
)
|
|
272
652
|
}
|
|
273
|
-
` :
|
|
653
|
+
` : d = `import { useEffect, useState } from 'react'
|
|
274
654
|
import { Engine } from '@doodle-engine/core'
|
|
275
655
|
import type { GameState, Snapshot } from '@doodle-engine/core'
|
|
276
|
-
import { GameProvider, useGame } from '@doodle-engine/react'
|
|
656
|
+
import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
|
|
277
657
|
|
|
278
658
|
export function App() {
|
|
279
659
|
const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
|
|
@@ -289,11 +669,11 @@ export function App() {
|
|
|
289
669
|
}, [])
|
|
290
670
|
|
|
291
671
|
if (!game) {
|
|
292
|
-
return <
|
|
672
|
+
return <LoadingScreen />
|
|
293
673
|
}
|
|
294
674
|
|
|
295
675
|
return (
|
|
296
|
-
<GameProvider engine={game.engine} initialSnapshot={game.snapshot}>
|
|
676
|
+
<GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
|
|
297
677
|
<GameUI />
|
|
298
678
|
</GameProvider>
|
|
299
679
|
)
|
|
@@ -361,7 +741,7 @@ function createEmptyState(): GameState {
|
|
|
361
741
|
currentLocale: 'en',
|
|
362
742
|
}
|
|
363
743
|
}
|
|
364
|
-
`, await
|
|
744
|
+
`, await g(s(e, "src/App.tsx"), d), await g(s(e, "src/index.css"), `body {
|
|
365
745
|
margin: 0;
|
|
366
746
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
367
747
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
@@ -369,7 +749,7 @@ function createEmptyState(): GameState {
|
|
|
369
749
|
-webkit-font-smoothing: antialiased;
|
|
370
750
|
-moz-osx-font-smoothing: grayscale;
|
|
371
751
|
}
|
|
372
|
-
`), console.log(
|
|
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
|
|
373
753
|
startLocation: tavern
|
|
374
754
|
startTime:
|
|
375
755
|
day: 1
|
|
@@ -380,40 +760,40 @@ startVariables:
|
|
|
380
760
|
reputation: 0
|
|
381
761
|
_drinksBought: 0
|
|
382
762
|
startInventory: []
|
|
383
|
-
`), await
|
|
763
|
+
`), await g(s(e, "content/locations/tavern.yaml"), `id: tavern
|
|
384
764
|
name: "@location.tavern.name"
|
|
385
765
|
description: "@location.tavern.description"
|
|
386
766
|
banner: ""
|
|
387
767
|
music: ""
|
|
388
768
|
ambient: ""
|
|
389
|
-
`), await
|
|
769
|
+
`), await g(s(e, "content/locations/market.yaml"), `id: market
|
|
390
770
|
name: "@location.market.name"
|
|
391
771
|
description: "@location.market.description"
|
|
392
772
|
banner: ""
|
|
393
773
|
music: ""
|
|
394
774
|
ambient: ""
|
|
395
|
-
`), await
|
|
775
|
+
`), await g(s(e, "content/characters/bartender.yaml"), `id: bartender
|
|
396
776
|
name: "@character.bartender.name"
|
|
397
777
|
biography: "@character.bartender.bio"
|
|
398
778
|
portrait: ""
|
|
399
779
|
location: tavern
|
|
400
780
|
dialogue: bartender_greeting
|
|
401
781
|
stats: {}
|
|
402
|
-
`), await
|
|
782
|
+
`), await g(s(e, "content/characters/merchant.yaml"), `id: merchant
|
|
403
783
|
name: "@character.merchant.name"
|
|
404
784
|
biography: "@character.merchant.bio"
|
|
405
785
|
portrait: ""
|
|
406
786
|
location: market
|
|
407
787
|
dialogue: merchant_intro
|
|
408
788
|
stats: {}
|
|
409
|
-
`), await
|
|
789
|
+
`), await g(s(e, "content/items/old_coin.yaml"), `id: old_coin
|
|
410
790
|
name: "@item.old_coin.name"
|
|
411
791
|
description: "@item.old_coin.description"
|
|
412
792
|
icon: ""
|
|
413
793
|
image: ""
|
|
414
794
|
location: tavern
|
|
415
795
|
stats: {}
|
|
416
|
-
`), await
|
|
796
|
+
`), await g(s(e, "content/maps/town.yaml"), `id: town
|
|
417
797
|
name: "@map.town.name"
|
|
418
798
|
image: ""
|
|
419
799
|
scale: 1
|
|
@@ -424,7 +804,7 @@ locations:
|
|
|
424
804
|
- id: market
|
|
425
805
|
x: 300
|
|
426
806
|
y: 150
|
|
427
|
-
`), await
|
|
807
|
+
`), await g(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
|
|
428
808
|
name: "@quest.odd_jobs.name"
|
|
429
809
|
description: "@quest.odd_jobs.description"
|
|
430
810
|
stages:
|
|
@@ -434,40 +814,60 @@ stages:
|
|
|
434
814
|
description: "@quest.odd_jobs.stage.talked_to_merchant"
|
|
435
815
|
- id: complete
|
|
436
816
|
description: "@quest.odd_jobs.stage.complete"
|
|
437
|
-
`), await
|
|
817
|
+
`), await g(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
|
|
438
818
|
title: "@journal.tavern_discovery.title"
|
|
439
819
|
text: "@journal.tavern_discovery.text"
|
|
440
820
|
category: places
|
|
441
|
-
`), await
|
|
821
|
+
`), await g(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
|
|
442
822
|
title: "@journal.odd_jobs_accepted.title"
|
|
443
823
|
text: "@journal.odd_jobs_accepted.text"
|
|
444
824
|
category: quests
|
|
445
|
-
`), await
|
|
825
|
+
`), await g(s(e, "content/journal/market_square.yaml"), `id: market_square
|
|
446
826
|
title: "@journal.market_square.title"
|
|
447
827
|
text: "@journal.market_square.text"
|
|
448
828
|
category: places
|
|
449
|
-
`), await
|
|
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
|
|
450
834
|
REQUIRE notFlag seenTavernIntro
|
|
451
835
|
|
|
836
|
+
# Each NODE is a conversation point. The first NODE is always the start.
|
|
452
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."
|
|
453
841
|
NARRATOR: @narrator.tavern_intro
|
|
842
|
+
|
|
843
|
+
# Effects run immediately when this node is reached, before choices are shown.
|
|
454
844
|
SET flag seenTavernIntro
|
|
455
845
|
|
|
846
|
+
# CHOICE text can use a @key or "inline text".
|
|
847
|
+
# A choice with END dialogue is a terminal choice — no GOTO needed.
|
|
456
848
|
CHOICE @narrator.choice.look_around
|
|
457
849
|
END dialogue
|
|
458
850
|
END
|
|
459
|
-
`), await
|
|
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
|
|
460
854
|
REQUIRE notFlag seenMarketIntro
|
|
461
855
|
|
|
462
856
|
NODE start
|
|
463
857
|
NARRATOR: @narrator.market_intro
|
|
464
858
|
SET flag seenMarketIntro
|
|
859
|
+
|
|
860
|
+
# ADD journalEntry unlocks a journal entry for the player.
|
|
465
861
|
ADD journalEntry market_square
|
|
466
862
|
|
|
467
863
|
CHOICE @narrator.choice.look_around
|
|
468
864
|
END dialogue
|
|
469
865
|
END
|
|
470
|
-
`), await
|
|
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
|
|
471
871
|
BARTENDER: @bartender.greeting
|
|
472
872
|
|
|
473
873
|
# Always available — ask for rumors (demonstrates: flag, relationship, journalEntry)
|
|
@@ -605,7 +1005,10 @@ NODE work_done
|
|
|
605
1005
|
NODE farewell
|
|
606
1006
|
BARTENDER: @bartender.farewell
|
|
607
1007
|
END dialogue
|
|
608
|
-
`), await
|
|
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
|
|
609
1012
|
MERCHANT: @merchant.greeting
|
|
610
1013
|
|
|
611
1014
|
CHOICE @merchant.choice.browse_wares
|
|
@@ -705,7 +1108,7 @@ NODE about_market
|
|
|
705
1108
|
NODE farewell
|
|
706
1109
|
MERCHANT: @merchant.farewell
|
|
707
1110
|
END dialogue
|
|
708
|
-
`), await
|
|
1111
|
+
`), await g(s(e, "content/locales/en.yaml"), `# ===================
|
|
709
1112
|
# Narrator Intros
|
|
710
1113
|
# ===================
|
|
711
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."
|
|
@@ -833,21 +1236,24 @@ notification.quest_updated: "Quest Updated: Odd Jobs"
|
|
|
833
1236
|
notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
|
|
834
1237
|
notification.bought_drink: "Bought a drink (-5 gold)"
|
|
835
1238
|
notification.bought_map: "Bought a map (-20 gold)"
|
|
836
|
-
`), console.log(
|
|
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
|
|
837
1240
|
dist
|
|
838
1241
|
.DS_Store
|
|
839
1242
|
*.log
|
|
840
1243
|
`);
|
|
841
1244
|
}
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
await
|
|
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);
|
|
1249
|
+
});
|
|
1250
|
+
E.command("dev").description("Start development server with hot reload").action(async () => {
|
|
1251
|
+
await Y();
|
|
846
1252
|
});
|
|
847
|
-
|
|
848
|
-
await
|
|
1253
|
+
E.command("build").description("Build game for production").action(async () => {
|
|
1254
|
+
await z();
|
|
849
1255
|
});
|
|
850
|
-
|
|
851
|
-
await
|
|
1256
|
+
E.command("validate").description("Validate game content").action(async () => {
|
|
1257
|
+
await Z();
|
|
852
1258
|
});
|
|
853
|
-
|
|
1259
|
+
E.parse();
|