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