@doodle-engine/cli 0.0.12 â 0.0.14
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 +724 -683
- package/dist/create.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command as G } from "commander";
|
|
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
|
-
function
|
|
13
|
-
const
|
|
14
|
-
for (const a of Object.values(
|
|
15
|
-
const
|
|
16
|
-
|
|
3
|
+
import { crayon as i } from "crayon.js";
|
|
4
|
+
import { createServer as $, build as H } from "vite";
|
|
5
|
+
import j from "@vitejs/plugin-react";
|
|
6
|
+
import { watch as M } from "chokidar";
|
|
7
|
+
import { readdir as p, readFile as f, mkdir as D, writeFile as k } from "fs/promises";
|
|
8
|
+
import { join as d, extname as g, relative as b, dirname as F } from "path";
|
|
9
|
+
import { parse as y } from "yaml";
|
|
10
|
+
import { parseDialogue as E } from "@doodle-engine/core";
|
|
11
|
+
import P from "prompts";
|
|
12
|
+
function I(n, o) {
|
|
13
|
+
const r = [];
|
|
14
|
+
for (const a of Object.values(n.dialogues)) {
|
|
15
|
+
const t = o.get(a.id) || `dialogue:${a.id}`;
|
|
16
|
+
r.push(...B(a, t));
|
|
17
17
|
}
|
|
18
|
-
for (const a of Object.values(
|
|
19
|
-
if (a.dialogue && !
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
file:
|
|
18
|
+
for (const a of Object.values(n.characters))
|
|
19
|
+
if (a.dialogue && !n.dialogues[a.dialogue]) {
|
|
20
|
+
const t = o.get(a.id) || `character:${a.id}`;
|
|
21
|
+
r.push({
|
|
22
|
+
file: t,
|
|
23
23
|
message: `Character "${a.id}" references non-existent dialogue "${a.dialogue}"`,
|
|
24
24
|
suggestion: `Create dialogue "${a.dialogue}" or fix the reference`
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
return
|
|
27
|
+
return r.push(...J(n, o)), r;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
for (const
|
|
32
|
-
a.has(
|
|
33
|
-
file:
|
|
34
|
-
message: `Duplicate node ID "${
|
|
29
|
+
function B(n, o) {
|
|
30
|
+
const r = [], a = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const t of n.nodes)
|
|
32
|
+
a.has(t.id) && r.push({
|
|
33
|
+
file: o,
|
|
34
|
+
message: `Duplicate node ID "${t.id}"`,
|
|
35
35
|
suggestion: "Node IDs must be unique within a dialogue"
|
|
36
|
-
}), a.add(
|
|
37
|
-
a.has(
|
|
38
|
-
file:
|
|
39
|
-
message: `Start node "${
|
|
40
|
-
suggestion: `Add a NODE ${
|
|
36
|
+
}), a.add(t.id);
|
|
37
|
+
a.has(n.startNode) || r.push({
|
|
38
|
+
file: o,
|
|
39
|
+
message: `Start node "${n.startNode}" not found`,
|
|
40
|
+
suggestion: `Add a NODE ${n.startNode} or fix the startNode reference`
|
|
41
41
|
});
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
return
|
|
42
|
+
for (const t of n.nodes)
|
|
43
|
+
r.push(...L(t, a, o));
|
|
44
|
+
return r;
|
|
45
45
|
}
|
|
46
|
-
function
|
|
46
|
+
function L(n, o, r) {
|
|
47
47
|
const a = [];
|
|
48
|
-
if (
|
|
49
|
-
file:
|
|
50
|
-
message: `Node "${
|
|
51
|
-
suggestion: `Add NODE ${
|
|
52
|
-
}),
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
file:
|
|
56
|
-
message: `Node "${
|
|
57
|
-
suggestion: `Add NODE ${
|
|
58
|
-
}), a.push(...
|
|
59
|
-
for (const
|
|
60
|
-
if (!
|
|
61
|
-
(
|
|
62
|
-
) && !
|
|
63
|
-
file:
|
|
64
|
-
message: `Node "${
|
|
65
|
-
suggestion: `Add NODE ${
|
|
66
|
-
}),
|
|
67
|
-
for (const
|
|
68
|
-
a.push(...
|
|
69
|
-
if (
|
|
70
|
-
for (const
|
|
71
|
-
a.push(...
|
|
48
|
+
if (n.next && !o.has(n.next) && a.push({
|
|
49
|
+
file: r,
|
|
50
|
+
message: `Node "${n.id}" GOTO "${n.next}" points to non-existent node`,
|
|
51
|
+
suggestion: `Add NODE ${n.next} or fix the GOTO target`
|
|
52
|
+
}), n.conditionalNext)
|
|
53
|
+
for (const t of n.conditionalNext)
|
|
54
|
+
o.has(t.next) || a.push({
|
|
55
|
+
file: r,
|
|
56
|
+
message: `Node "${n.id}" IF block GOTO "${t.next}" points to non-existent node`,
|
|
57
|
+
suggestion: `Add NODE ${t.next} or fix the GOTO target`
|
|
58
|
+
}), a.push(...v(t.condition, n.id, r));
|
|
59
|
+
for (const t of n.choices) {
|
|
60
|
+
if (!t.effects?.some(
|
|
61
|
+
(e) => e.type === "endDialogue" || e.type === "goToLocation"
|
|
62
|
+
) && !o.has(t.next) && a.push({
|
|
63
|
+
file: r,
|
|
64
|
+
message: `Node "${n.id}" choice "${t.id}" GOTO "${t.next}" points to non-existent node`,
|
|
65
|
+
suggestion: `Add NODE ${t.next} or fix the GOTO target`
|
|
66
|
+
}), t.conditions)
|
|
67
|
+
for (const e of t.conditions)
|
|
68
|
+
a.push(...v(e, n.id, r));
|
|
69
|
+
if (t.effects)
|
|
70
|
+
for (const e of t.effects)
|
|
71
|
+
a.push(...C(e, n.id, r));
|
|
72
72
|
}
|
|
73
|
-
if (
|
|
74
|
-
for (const
|
|
75
|
-
a.push(...
|
|
76
|
-
if (
|
|
77
|
-
for (const
|
|
78
|
-
a.push(...
|
|
73
|
+
if (n.conditions)
|
|
74
|
+
for (const t of n.conditions)
|
|
75
|
+
a.push(...v(t, n.id, r));
|
|
76
|
+
if (n.effects)
|
|
77
|
+
for (const t of n.effects)
|
|
78
|
+
a.push(...C(t, n.id, r));
|
|
79
79
|
return a;
|
|
80
80
|
}
|
|
81
|
-
const
|
|
81
|
+
const U = {
|
|
82
82
|
hasFlag: ["flag"],
|
|
83
83
|
notFlag: ["flag"],
|
|
84
84
|
hasItem: ["itemId"],
|
|
@@ -94,7 +94,7 @@ const L = {
|
|
|
94
94
|
relationshipBelow: ["characterId", "value"],
|
|
95
95
|
itemAt: ["itemId", "locationId"],
|
|
96
96
|
roll: ["min", "max", "threshold"]
|
|
97
|
-
},
|
|
97
|
+
}, W = {
|
|
98
98
|
setFlag: ["flag"],
|
|
99
99
|
clearFlag: ["flag"],
|
|
100
100
|
setVariable: ["variable", "value"],
|
|
@@ -123,138 +123,138 @@ const L = {
|
|
|
123
123
|
showInterlude: ["interludeId"],
|
|
124
124
|
roll: ["variable", "min", "max"]
|
|
125
125
|
};
|
|
126
|
-
function
|
|
126
|
+
function v(n, o, r) {
|
|
127
127
|
const a = [];
|
|
128
|
-
if (!
|
|
128
|
+
if (!n.type)
|
|
129
129
|
return a.push({
|
|
130
|
-
file:
|
|
131
|
-
message: `Node "${
|
|
130
|
+
file: r,
|
|
131
|
+
message: `Node "${o}" has condition with missing type`
|
|
132
132
|
}), a;
|
|
133
|
-
if (
|
|
134
|
-
return
|
|
135
|
-
file:
|
|
136
|
-
message: `Node "${
|
|
133
|
+
if (n.type === "timeIs")
|
|
134
|
+
return n.hour === void 0 && n.day === void 0 && a.push({
|
|
135
|
+
file: r,
|
|
136
|
+
message: `Node "${o}" condition "timeIs" must have at least one of "hour" or "day" argument`
|
|
137
137
|
}), a;
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
138
|
+
const t = U[n.type];
|
|
139
|
+
if (!t)
|
|
140
140
|
return a;
|
|
141
|
-
for (const
|
|
142
|
-
(
|
|
143
|
-
file:
|
|
144
|
-
message: `Node "${
|
|
141
|
+
for (const s of t)
|
|
142
|
+
(n[s] === void 0 || n[s] === null || n[s] === "") && a.push({
|
|
143
|
+
file: r,
|
|
144
|
+
message: `Node "${o}" condition "${n.type}" missing required "${s}" argument`
|
|
145
145
|
});
|
|
146
146
|
return a;
|
|
147
147
|
}
|
|
148
|
-
function
|
|
148
|
+
function C(n, o, r) {
|
|
149
149
|
const a = [];
|
|
150
|
-
if (!
|
|
150
|
+
if (!n.type)
|
|
151
151
|
return a.push({
|
|
152
|
-
file:
|
|
153
|
-
message: `Node "${
|
|
152
|
+
file: r,
|
|
153
|
+
message: `Node "${o}" has effect with missing type`
|
|
154
154
|
}), a;
|
|
155
|
-
const
|
|
156
|
-
if (!
|
|
155
|
+
const t = W[n.type];
|
|
156
|
+
if (!t)
|
|
157
157
|
return a;
|
|
158
|
-
for (const
|
|
159
|
-
(
|
|
160
|
-
file:
|
|
161
|
-
message: `Node "${
|
|
158
|
+
for (const s of t)
|
|
159
|
+
(n[s] === void 0 || n[s] === null || n[s] === "") && a.push({
|
|
160
|
+
file: r,
|
|
161
|
+
message: `Node "${o}" effect "${n.type}" missing required "${s}" argument`
|
|
162
162
|
});
|
|
163
163
|
return a;
|
|
164
164
|
}
|
|
165
|
-
function J(
|
|
166
|
-
const
|
|
167
|
-
for (const
|
|
168
|
-
for (const
|
|
169
|
-
a.add(
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
if (!a.has(
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
file:
|
|
176
|
-
message: `Localization key "${
|
|
177
|
-
suggestion: `Add "${
|
|
165
|
+
function J(n, o) {
|
|
166
|
+
const r = [], a = /* @__PURE__ */ new Set();
|
|
167
|
+
for (const e of Object.values(n.locales))
|
|
168
|
+
for (const c of Object.keys(e))
|
|
169
|
+
a.add(c);
|
|
170
|
+
const t = (e) => e.startsWith("@"), s = (e, c, l) => {
|
|
171
|
+
const u = e.slice(1);
|
|
172
|
+
if (!a.has(u)) {
|
|
173
|
+
const h = o.get(c) || `${l}:${c}`;
|
|
174
|
+
r.push({
|
|
175
|
+
file: h,
|
|
176
|
+
message: `Localization key "${e}" not found in any locale file`,
|
|
177
|
+
suggestion: `Add "${u}: ..." to your locale files`
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
};
|
|
181
|
-
for (const
|
|
182
|
-
|
|
183
|
-
for (const
|
|
184
|
-
|
|
185
|
-
for (const
|
|
186
|
-
|
|
187
|
-
for (const
|
|
188
|
-
|
|
189
|
-
for (const
|
|
190
|
-
|
|
181
|
+
for (const e of Object.values(n.locations))
|
|
182
|
+
t(e.name) && s(e.name, e.id, "location"), t(e.description) && s(e.description, e.id, "location");
|
|
183
|
+
for (const e of Object.values(n.characters))
|
|
184
|
+
t(e.name) && s(e.name, e.id, "character"), t(e.biography) && s(e.biography, e.id, "character");
|
|
185
|
+
for (const e of Object.values(n.items))
|
|
186
|
+
t(e.name) && s(e.name, e.id, "item"), t(e.description) && s(e.description, e.id, "item");
|
|
187
|
+
for (const e of Object.values(n.quests)) {
|
|
188
|
+
t(e.name) && s(e.name, e.id, "quest"), t(e.description) && s(e.description, e.id, "quest");
|
|
189
|
+
for (const c of e.stages)
|
|
190
|
+
t(c.description) && s(c.description, e.id, "quest");
|
|
191
191
|
}
|
|
192
|
-
for (const
|
|
193
|
-
|
|
194
|
-
for (const
|
|
195
|
-
for (const
|
|
196
|
-
|
|
197
|
-
for (const
|
|
198
|
-
|
|
192
|
+
for (const e of Object.values(n.journalEntries))
|
|
193
|
+
t(e.title) && s(e.title, e.id, "journal"), t(e.text) && s(e.text, e.id, "journal");
|
|
194
|
+
for (const e of Object.values(n.dialogues))
|
|
195
|
+
for (const c of e.nodes) {
|
|
196
|
+
t(c.text) && s(c.text, e.id, "dialogue");
|
|
197
|
+
for (const l of c.choices)
|
|
198
|
+
t(l.text) && s(l.text, e.id, "dialogue");
|
|
199
199
|
}
|
|
200
|
-
for (const
|
|
201
|
-
|
|
202
|
-
return
|
|
200
|
+
for (const e of Object.values(n.interludes))
|
|
201
|
+
t(e.text) && s(e.text, e.id, "interlude");
|
|
202
|
+
return r;
|
|
203
203
|
}
|
|
204
|
-
function
|
|
205
|
-
if (
|
|
206
|
-
console.log(
|
|
204
|
+
function N(n) {
|
|
205
|
+
if (n.length === 0) {
|
|
206
|
+
console.log(i.green("â No validation errors"));
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
|
-
console.log(
|
|
210
|
-
â Found ${
|
|
209
|
+
console.log(i.red(`
|
|
210
|
+
â Found ${n.length} validation error${n.length === 1 ? "" : "s"}:
|
|
211
211
|
`));
|
|
212
|
-
for (const
|
|
213
|
-
console.log(
|
|
212
|
+
for (const o of n)
|
|
213
|
+
console.log(i.bold(o.file) + (o.line ? `:${o.line}` : "")), console.log(" " + i.red(o.message)), o.suggestion && console.log(" " + i.dim(o.suggestion)), console.log();
|
|
214
214
|
}
|
|
215
|
-
const
|
|
216
|
-
async function
|
|
217
|
-
const
|
|
218
|
-
console.log(""), console.log(
|
|
219
|
-
const
|
|
215
|
+
const R = "ðū", Q = "âĻ", Y = "âïļ", V = "â";
|
|
216
|
+
async function K() {
|
|
217
|
+
const n = process.cwd(), o = d(n, "content");
|
|
218
|
+
console.log(""), console.log(i.bold.magenta(` ${R} Doodle Engine Dev Server ${R}`)), console.log("");
|
|
219
|
+
const r = {
|
|
220
220
|
name: "doodle-content-loader",
|
|
221
|
-
configureServer(
|
|
222
|
-
|
|
221
|
+
configureServer(t) {
|
|
222
|
+
t.middlewares.use("/api/content", async (e, c) => {
|
|
223
223
|
try {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
} catch (
|
|
227
|
-
console.error(
|
|
224
|
+
const l = await z(o);
|
|
225
|
+
c.setHeader("Content-Type", "application/json"), c.end(JSON.stringify(l));
|
|
226
|
+
} catch (l) {
|
|
227
|
+
console.error(i.red(" Error loading content:"), l), c.statusCode = 500, c.end(JSON.stringify({ error: "Failed to load content" }));
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
|
-
const
|
|
230
|
+
const s = M(o, {
|
|
231
231
|
ignored: /(^|[\/\\])\../,
|
|
232
232
|
persistent: !0
|
|
233
233
|
});
|
|
234
|
-
|
|
235
|
-
console.log(
|
|
234
|
+
s.on("change", async (e) => {
|
|
235
|
+
console.log(i.yellow(` ${Y} Content changed: ${e}`)), await A(o), t.ws.send({
|
|
236
236
|
type: "full-reload",
|
|
237
237
|
path: "*"
|
|
238
238
|
});
|
|
239
|
-
}),
|
|
240
|
-
console.log(
|
|
239
|
+
}), s.on("add", async (e) => {
|
|
240
|
+
console.log(i.green(` ${V} Content added: ${e}`)), await A(o), t.ws.send({
|
|
241
241
|
type: "full-reload",
|
|
242
242
|
path: "*"
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
-
}, a = await
|
|
247
|
-
root:
|
|
248
|
-
plugins: [
|
|
246
|
+
}, a = await $({
|
|
247
|
+
root: n,
|
|
248
|
+
plugins: [j(), r],
|
|
249
249
|
server: {
|
|
250
250
|
port: 3e3,
|
|
251
251
|
open: !0
|
|
252
252
|
}
|
|
253
253
|
});
|
|
254
|
-
await a.listen(), a.printUrls(), console.log(""), console.log(
|
|
254
|
+
await a.listen(), a.printUrls(), console.log(""), console.log(i.dim(` ${Q} Watching content files for changes...`)), console.log("");
|
|
255
255
|
}
|
|
256
|
-
async function
|
|
257
|
-
const
|
|
256
|
+
async function z(n) {
|
|
257
|
+
const o = {
|
|
258
258
|
locations: {},
|
|
259
259
|
characters: {},
|
|
260
260
|
items: {},
|
|
@@ -265,7 +265,7 @@ async function P(e) {
|
|
|
265
265
|
interludes: {},
|
|
266
266
|
locales: {}
|
|
267
267
|
};
|
|
268
|
-
let
|
|
268
|
+
let r = null;
|
|
269
269
|
const a = [
|
|
270
270
|
{ dir: "locations", key: "locations" },
|
|
271
271
|
{ dir: "characters", key: "characters" },
|
|
@@ -275,41 +275,41 @@ async function P(e) {
|
|
|
275
275
|
{ dir: "journal", key: "journalEntries" },
|
|
276
276
|
{ dir: "interludes", key: "interludes" }
|
|
277
277
|
];
|
|
278
|
-
for (const { dir:
|
|
279
|
-
const
|
|
278
|
+
for (const { dir: t, key: s } of a) {
|
|
279
|
+
const e = d(n, t);
|
|
280
280
|
try {
|
|
281
|
-
const
|
|
282
|
-
for (const
|
|
283
|
-
if (
|
|
284
|
-
const
|
|
285
|
-
|
|
281
|
+
const c = await p(e);
|
|
282
|
+
for (const l of c)
|
|
283
|
+
if (g(l) === ".yaml" || g(l) === ".yml") {
|
|
284
|
+
const u = d(e, l), h = await f(u, "utf-8"), m = y(h);
|
|
285
|
+
m && m.id && (o[s][m.id] = m);
|
|
286
286
|
}
|
|
287
287
|
} catch {
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
try {
|
|
291
|
-
const
|
|
292
|
-
for (const
|
|
293
|
-
if (
|
|
294
|
-
const
|
|
295
|
-
|
|
291
|
+
const t = d(n, "locales"), s = await p(t);
|
|
292
|
+
for (const e of s)
|
|
293
|
+
if (g(e) === ".yaml" || g(e) === ".yml") {
|
|
294
|
+
const c = d(t, e), l = await f(c, "utf-8"), u = y(l), h = e.replace(/\.(yaml|yml)$/, "");
|
|
295
|
+
o.locales[h] = u ?? {};
|
|
296
296
|
}
|
|
297
297
|
} catch {
|
|
298
298
|
}
|
|
299
299
|
try {
|
|
300
|
-
const
|
|
301
|
-
for (const
|
|
302
|
-
if (
|
|
303
|
-
const
|
|
304
|
-
|
|
300
|
+
const t = d(n, "dialogues"), s = await p(t);
|
|
301
|
+
for (const e of s)
|
|
302
|
+
if (g(e) === ".dlg") {
|
|
303
|
+
const c = d(t, e), l = await f(c, "utf-8"), u = e.replace(".dlg", ""), h = E(l, u);
|
|
304
|
+
o.dialogues[h.id] = h;
|
|
305
305
|
}
|
|
306
306
|
} catch {
|
|
307
307
|
}
|
|
308
308
|
try {
|
|
309
|
-
const
|
|
310
|
-
|
|
309
|
+
const t = d(n, "game.yaml"), s = await f(t, "utf-8");
|
|
310
|
+
r = y(s);
|
|
311
311
|
} catch {
|
|
312
|
-
console.warn(
|
|
312
|
+
console.warn(i.yellow(" No game.yaml found, using defaults")), r = {
|
|
313
313
|
startLocation: "tavern",
|
|
314
314
|
startTime: { day: 1, hour: 8 },
|
|
315
315
|
startFlags: {},
|
|
@@ -317,18 +317,18 @@ async function P(e) {
|
|
|
317
317
|
startInventory: []
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
|
-
return { registry:
|
|
320
|
+
return { registry: o, config: r };
|
|
321
321
|
}
|
|
322
|
-
async function
|
|
322
|
+
async function A(n) {
|
|
323
323
|
try {
|
|
324
|
-
const { registry:
|
|
325
|
-
a.length > 0 && (console.log(""),
|
|
326
|
-
} catch (
|
|
327
|
-
console.error(
|
|
324
|
+
const { registry: o, fileMap: r } = await X(n), a = I(o, r);
|
|
325
|
+
a.length > 0 && (console.log(""), N(a), console.log(""));
|
|
326
|
+
} catch (o) {
|
|
327
|
+
console.error(i.red(" Error running validation:"), o);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
-
async function
|
|
331
|
-
const
|
|
330
|
+
async function X(n) {
|
|
331
|
+
const o = {
|
|
332
332
|
locations: {},
|
|
333
333
|
characters: {},
|
|
334
334
|
items: {},
|
|
@@ -338,7 +338,7 @@ async function K(e) {
|
|
|
338
338
|
journalEntries: {},
|
|
339
339
|
interludes: {},
|
|
340
340
|
locales: {}
|
|
341
|
-
},
|
|
341
|
+
}, r = /* @__PURE__ */ new Map(), a = [
|
|
342
342
|
{ dir: "locations", key: "locations" },
|
|
343
343
|
{ dir: "characters", key: "characters" },
|
|
344
344
|
{ dir: "items", key: "items" },
|
|
@@ -347,66 +347,66 @@ async function K(e) {
|
|
|
347
347
|
{ dir: "journal", key: "journalEntries" },
|
|
348
348
|
{ dir: "interludes", key: "interludes" }
|
|
349
349
|
];
|
|
350
|
-
for (const { dir:
|
|
351
|
-
const
|
|
350
|
+
for (const { dir: t, key: s } of a) {
|
|
351
|
+
const e = d(n, t);
|
|
352
352
|
try {
|
|
353
|
-
const
|
|
354
|
-
for (const
|
|
355
|
-
if (
|
|
356
|
-
const
|
|
357
|
-
|
|
353
|
+
const c = await p(e);
|
|
354
|
+
for (const l of c)
|
|
355
|
+
if (g(l) === ".yaml" || g(l) === ".yml") {
|
|
356
|
+
const u = d(e, l), h = await f(u, "utf-8"), m = y(h);
|
|
357
|
+
m && m.id && (o[s][m.id] = m, r.set(m.id, b(process.cwd(), u)));
|
|
358
358
|
}
|
|
359
359
|
} catch {
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
try {
|
|
363
|
-
const
|
|
364
|
-
for (const
|
|
365
|
-
if (
|
|
366
|
-
const
|
|
367
|
-
|
|
363
|
+
const t = d(n, "locales"), s = await p(t);
|
|
364
|
+
for (const e of s)
|
|
365
|
+
if (g(e) === ".yaml" || g(e) === ".yml") {
|
|
366
|
+
const c = d(t, e), l = await f(c, "utf-8"), u = y(l), h = e.replace(/\.(yaml|yml)$/, "");
|
|
367
|
+
o.locales[h] = u ?? {};
|
|
368
368
|
}
|
|
369
369
|
} catch {
|
|
370
370
|
}
|
|
371
371
|
try {
|
|
372
|
-
const
|
|
373
|
-
for (const
|
|
374
|
-
if (
|
|
375
|
-
const
|
|
376
|
-
|
|
372
|
+
const t = d(n, "dialogues"), s = await p(t);
|
|
373
|
+
for (const e of s)
|
|
374
|
+
if (g(e) === ".dlg") {
|
|
375
|
+
const c = d(t, e), l = await f(c, "utf-8"), u = e.replace(".dlg", ""), h = E(l, u);
|
|
376
|
+
o.dialogues[h.id] = h, r.set(h.id, b(process.cwd(), c));
|
|
377
377
|
}
|
|
378
378
|
} catch {
|
|
379
379
|
}
|
|
380
|
-
return { registry:
|
|
380
|
+
return { registry: o, fileMap: r };
|
|
381
381
|
}
|
|
382
|
-
async function
|
|
383
|
-
const
|
|
384
|
-
console.log(""), console.log(
|
|
385
|
-
let
|
|
382
|
+
async function Z() {
|
|
383
|
+
const n = process.cwd(), o = d(n, "content");
|
|
384
|
+
console.log(""), console.log(i.bold.magenta("ð Building Doodle Engine game...")), console.log(""), console.log(i.dim("Validating content..."));
|
|
385
|
+
let r;
|
|
386
386
|
try {
|
|
387
|
-
const { registry: a, fileMap:
|
|
388
|
-
|
|
387
|
+
const { registry: a, fileMap: t, config: s } = await ee(o), e = I(a, t);
|
|
388
|
+
N(e), e.length > 0 && (console.log(i.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), r = { registry: a, config: s };
|
|
389
389
|
} catch (a) {
|
|
390
|
-
console.error(
|
|
390
|
+
console.error(i.red("Error loading content:"), a), process.exit(1);
|
|
391
391
|
}
|
|
392
392
|
console.log("");
|
|
393
393
|
try {
|
|
394
|
-
await
|
|
395
|
-
root:
|
|
396
|
-
plugins: [
|
|
394
|
+
await H({
|
|
395
|
+
root: n,
|
|
396
|
+
plugins: [j()],
|
|
397
397
|
build: {
|
|
398
398
|
outDir: "dist",
|
|
399
399
|
emptyOutDir: !0
|
|
400
400
|
}
|
|
401
401
|
});
|
|
402
|
-
const a =
|
|
403
|
-
await
|
|
402
|
+
const a = d(n, "dist", "api");
|
|
403
|
+
await D(a, { recursive: !0 }), await k(d(a, "content"), JSON.stringify(r)), console.log(""), console.log(i.green("â
Build complete! Output in dist/")), console.log(""), console.log("To preview the build:"), console.log(i.dim(" yarn preview")), console.log("");
|
|
404
404
|
} catch (a) {
|
|
405
|
-
console.error(
|
|
405
|
+
console.error(i.red("Build failed:"), a), process.exit(1);
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
-
async function
|
|
409
|
-
const
|
|
408
|
+
async function ee(n) {
|
|
409
|
+
const o = {
|
|
410
410
|
locations: {},
|
|
411
411
|
characters: {},
|
|
412
412
|
items: {},
|
|
@@ -416,7 +416,7 @@ async function X(e) {
|
|
|
416
416
|
journalEntries: {},
|
|
417
417
|
interludes: {},
|
|
418
418
|
locales: {}
|
|
419
|
-
},
|
|
419
|
+
}, r = /* @__PURE__ */ new Map(), a = [
|
|
420
420
|
{ dir: "locations", key: "locations" },
|
|
421
421
|
{ dir: "characters", key: "characters" },
|
|
422
422
|
{ dir: "items", key: "items" },
|
|
@@ -425,57 +425,57 @@ async function X(e) {
|
|
|
425
425
|
{ dir: "journal", key: "journalEntries" },
|
|
426
426
|
{ dir: "interludes", key: "interludes" }
|
|
427
427
|
];
|
|
428
|
-
for (const { dir:
|
|
429
|
-
const
|
|
428
|
+
for (const { dir: s, key: e } of a) {
|
|
429
|
+
const c = d(n, s);
|
|
430
430
|
try {
|
|
431
|
-
const
|
|
432
|
-
for (const
|
|
433
|
-
if (
|
|
434
|
-
const
|
|
435
|
-
|
|
431
|
+
const l = await p(c);
|
|
432
|
+
for (const u of l)
|
|
433
|
+
if (g(u) === ".yaml" || g(u) === ".yml") {
|
|
434
|
+
const h = d(c, u), m = await f(h, "utf-8"), w = y(m);
|
|
435
|
+
w && w.id && (o[e][w.id] = w, r.set(w.id, b(process.cwd(), h)));
|
|
436
436
|
}
|
|
437
437
|
} catch {
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
try {
|
|
441
|
-
const
|
|
442
|
-
for (const
|
|
443
|
-
if (
|
|
444
|
-
const
|
|
445
|
-
|
|
441
|
+
const s = d(n, "locales"), e = await p(s);
|
|
442
|
+
for (const c of e)
|
|
443
|
+
if (g(c) === ".yaml" || g(c) === ".yml") {
|
|
444
|
+
const l = d(s, c), u = await f(l, "utf-8"), h = y(u), m = c.replace(/\.(yaml|yml)$/, "");
|
|
445
|
+
o.locales[m] = h ?? {};
|
|
446
446
|
}
|
|
447
447
|
} catch {
|
|
448
448
|
}
|
|
449
449
|
try {
|
|
450
|
-
const
|
|
451
|
-
for (const
|
|
452
|
-
if (
|
|
453
|
-
const
|
|
454
|
-
|
|
450
|
+
const s = d(n, "dialogues"), e = await p(s);
|
|
451
|
+
for (const c of e)
|
|
452
|
+
if (g(c) === ".dlg") {
|
|
453
|
+
const l = d(s, c), u = await f(l, "utf-8"), h = c.replace(".dlg", ""), m = E(u, h);
|
|
454
|
+
o.dialogues[m.id] = m, r.set(m.id, b(process.cwd(), l));
|
|
455
455
|
}
|
|
456
456
|
} catch {
|
|
457
457
|
}
|
|
458
|
-
let
|
|
458
|
+
let t = null;
|
|
459
459
|
try {
|
|
460
|
-
const
|
|
461
|
-
|
|
460
|
+
const s = d(n, "game.yaml"), e = await f(s, "utf-8");
|
|
461
|
+
t = y(e);
|
|
462
462
|
} catch {
|
|
463
|
-
|
|
463
|
+
t = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
|
|
464
464
|
}
|
|
465
|
-
return { registry:
|
|
465
|
+
return { registry: o, fileMap: r, config: t };
|
|
466
466
|
}
|
|
467
|
-
async function
|
|
468
|
-
const
|
|
469
|
-
console.log(""), console.log(
|
|
467
|
+
async function ne() {
|
|
468
|
+
const n = process.cwd(), o = d(n, "content");
|
|
469
|
+
console.log(""), console.log(i.bold.magenta("ðū Validating Doodle Engine content...")), console.log("");
|
|
470
470
|
try {
|
|
471
|
-
const { registry:
|
|
472
|
-
|
|
473
|
-
} catch (
|
|
474
|
-
console.error(
|
|
471
|
+
const { registry: r, fileMap: a } = await te(o), t = I(r, a);
|
|
472
|
+
N(t), t.length > 0 && process.exit(1);
|
|
473
|
+
} catch (r) {
|
|
474
|
+
console.error(i.red("Error loading content:"), r), process.exit(1);
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
|
-
async function
|
|
478
|
-
const
|
|
477
|
+
async function te(n) {
|
|
478
|
+
const o = {
|
|
479
479
|
locations: {},
|
|
480
480
|
characters: {},
|
|
481
481
|
items: {},
|
|
@@ -484,7 +484,7 @@ async function ee(e) {
|
|
|
484
484
|
quests: {},
|
|
485
485
|
journalEntries: {},
|
|
486
486
|
locales: {}
|
|
487
|
-
},
|
|
487
|
+
}, r = /* @__PURE__ */ new Map(), a = [
|
|
488
488
|
{ dir: "locations", key: "locations" },
|
|
489
489
|
{ dir: "characters", key: "characters" },
|
|
490
490
|
{ dir: "items", key: "items" },
|
|
@@ -492,124 +492,43 @@ async function ee(e) {
|
|
|
492
492
|
{ dir: "quests", key: "quests" },
|
|
493
493
|
{ dir: "journal", key: "journalEntries" }
|
|
494
494
|
];
|
|
495
|
-
for (const { dir:
|
|
496
|
-
const
|
|
495
|
+
for (const { dir: t, key: s } of a) {
|
|
496
|
+
const e = d(n, t);
|
|
497
497
|
try {
|
|
498
|
-
const
|
|
499
|
-
for (const
|
|
500
|
-
if (
|
|
501
|
-
const
|
|
502
|
-
|
|
498
|
+
const c = await p(e);
|
|
499
|
+
for (const l of c)
|
|
500
|
+
if (g(l) === ".yaml" || g(l) === ".yml") {
|
|
501
|
+
const u = d(e, l), h = await f(u, "utf-8"), m = y(h);
|
|
502
|
+
m && m.id && (o[s][m.id] = m, r.set(m.id, b(process.cwd(), u)));
|
|
503
503
|
}
|
|
504
504
|
} catch {
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
try {
|
|
508
|
-
const
|
|
509
|
-
for (const
|
|
510
|
-
if (
|
|
511
|
-
const
|
|
512
|
-
|
|
508
|
+
const t = d(n, "locales"), s = await p(t);
|
|
509
|
+
for (const e of s)
|
|
510
|
+
if (g(e) === ".yaml" || g(e) === ".yml") {
|
|
511
|
+
const c = d(t, e), l = await f(c, "utf-8"), u = y(l), h = e.replace(/\.(yaml|yml)$/, "");
|
|
512
|
+
o.locales[h] = u ?? {};
|
|
513
513
|
}
|
|
514
514
|
} catch {
|
|
515
515
|
}
|
|
516
516
|
try {
|
|
517
|
-
const
|
|
518
|
-
for (const
|
|
519
|
-
if (
|
|
520
|
-
const
|
|
521
|
-
|
|
517
|
+
const t = d(n, "dialogues"), s = await p(t);
|
|
518
|
+
for (const e of s)
|
|
519
|
+
if (g(e) === ".dlg") {
|
|
520
|
+
const c = d(t, e), l = await f(c, "utf-8"), u = e.replace(".dlg", ""), h = E(l, u);
|
|
521
|
+
o.dialogues[h.id] = h, r.set(h.id, b(process.cwd(), c));
|
|
522
522
|
}
|
|
523
523
|
} catch {
|
|
524
524
|
}
|
|
525
|
-
return { registry:
|
|
526
|
-
}
|
|
527
|
-
const D = "ðū", te = "ð", x = "ðĶī", oe = "âĻ", q = "ð", _ = "â
", ae = "ð";
|
|
528
|
-
async function ne(e) {
|
|
529
|
-
const n = s(process.cwd(), e);
|
|
530
|
-
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("");
|
|
531
|
-
const { useDefaultRenderer: i } = await M({
|
|
532
|
-
type: "confirm",
|
|
533
|
-
name: "useDefaultRenderer",
|
|
534
|
-
message: "Use default renderer?",
|
|
535
|
-
initial: !0
|
|
536
|
-
});
|
|
537
|
-
i === void 0 && (console.log(c.yellow(`
|
|
538
|
-
${x} 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(` ${q} ${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("");
|
|
525
|
+
return { registry: o, fileMap: r };
|
|
539
526
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"content/dialogues",
|
|
546
|
-
"content/quests",
|
|
547
|
-
"content/journal",
|
|
548
|
-
"content/interludes",
|
|
549
|
-
"content/locales",
|
|
550
|
-
"content/maps",
|
|
551
|
-
"assets/images/banners",
|
|
552
|
-
"assets/images/portraits",
|
|
553
|
-
"assets/images/items",
|
|
554
|
-
"assets/images/maps",
|
|
555
|
-
"assets/audio/music",
|
|
556
|
-
"assets/audio/sfx",
|
|
557
|
-
"assets/audio/voice",
|
|
558
|
-
"src"
|
|
559
|
-
];
|
|
560
|
-
console.log(` ${q} ${c.bold("Creating directories...")}`);
|
|
561
|
-
for (const u of a)
|
|
562
|
-
await A(s(e, u), { recursive: !0 });
|
|
563
|
-
console.log(c.green(` ${_} Directories created`)), console.log("");
|
|
564
|
-
const o = {
|
|
565
|
-
name: n,
|
|
566
|
-
version: "0.1.0",
|
|
567
|
-
type: "module",
|
|
568
|
-
scripts: {
|
|
569
|
-
dev: "doodle dev",
|
|
570
|
-
build: "doodle build",
|
|
571
|
-
preview: "vite preview"
|
|
572
|
-
},
|
|
573
|
-
dependencies: {
|
|
574
|
-
"@doodle-engine/core": "latest",
|
|
575
|
-
"@doodle-engine/react": "latest",
|
|
576
|
-
react: "^19.0.0",
|
|
577
|
-
"react-dom": "^19.0.0"
|
|
578
|
-
},
|
|
579
|
-
devDependencies: {
|
|
580
|
-
"@doodle-engine/cli": "latest",
|
|
581
|
-
"@types/react": "^19.0.0",
|
|
582
|
-
"@types/react-dom": "^19.0.0",
|
|
583
|
-
"@vitejs/plugin-react": "^4.3.0",
|
|
584
|
-
typescript: "^5.7.0",
|
|
585
|
-
vite: "^6.0.0"
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
console.log(` ${oe} ${c.bold("Writing project files...")}`), await m(
|
|
589
|
-
s(e, "package.json"),
|
|
590
|
-
JSON.stringify(o, null, 2)
|
|
591
|
-
);
|
|
592
|
-
const r = {
|
|
593
|
-
compilerOptions: {
|
|
594
|
-
target: "ES2024",
|
|
595
|
-
lib: ["ES2024", "DOM", "DOM.Iterable"],
|
|
596
|
-
module: "ESNext",
|
|
597
|
-
moduleResolution: "bundler",
|
|
598
|
-
jsx: "react-jsx",
|
|
599
|
-
strict: !0,
|
|
600
|
-
skipLibCheck: !0,
|
|
601
|
-
esModuleInterop: !0,
|
|
602
|
-
forceConsistentCasingInFileNames: !0,
|
|
603
|
-
resolveJsonModule: !0,
|
|
604
|
-
isolatedModules: !0,
|
|
605
|
-
noEmit: !0
|
|
606
|
-
},
|
|
607
|
-
include: ["src"]
|
|
608
|
-
};
|
|
609
|
-
await m(
|
|
610
|
-
s(e, "tsconfig.json"),
|
|
611
|
-
JSON.stringify(r, null, 2)
|
|
612
|
-
), await m(s(e, "index.html"), `<!doctype html>
|
|
527
|
+
const oe = `node_modules
|
|
528
|
+
dist
|
|
529
|
+
.DS_Store
|
|
530
|
+
*.log
|
|
531
|
+
`, ae = `<!doctype html>
|
|
613
532
|
<html lang="en">
|
|
614
533
|
<head>
|
|
615
534
|
<meta charset="UTF-8" />
|
|
@@ -621,298 +540,40 @@ async function re(e, n, i) {
|
|
|
621
540
|
<script type="module" src="/src/main.tsx"><\/script>
|
|
622
541
|
</body>
|
|
623
542
|
</html>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
export function App() {
|
|
641
|
-
const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
|
|
642
|
-
|
|
643
|
-
useEffect(() => {
|
|
644
|
-
fetch('/api/content')
|
|
645
|
-
.then(res => res.json())
|
|
646
|
-
.then(data => setContent({ registry: data.registry, config: data.config }))
|
|
647
|
-
}, [])
|
|
648
|
-
|
|
649
|
-
if (!content) {
|
|
650
|
-
return <LoadingScreen />
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return (
|
|
654
|
-
<GameShell
|
|
655
|
-
registry={content.registry}
|
|
656
|
-
config={content.config}
|
|
657
|
-
title="My Doodle Game"
|
|
658
|
-
subtitle="A text-based adventure"
|
|
659
|
-
splashDuration={2000}
|
|
660
|
-
availableLocales={[{ code: 'en', label: 'English' }]}
|
|
661
|
-
devTools={import.meta.env.DEV}
|
|
662
|
-
/>
|
|
663
|
-
)
|
|
664
|
-
}
|
|
665
|
-
` : d = `import { useEffect, useState } from 'react'
|
|
666
|
-
import { Engine } from '@doodle-engine/core'
|
|
667
|
-
import type { GameState, Snapshot } from '@doodle-engine/core'
|
|
668
|
-
import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
|
|
669
|
-
|
|
670
|
-
export function App() {
|
|
671
|
-
const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
|
|
672
|
-
|
|
673
|
-
useEffect(() => {
|
|
674
|
-
fetch('/api/content')
|
|
675
|
-
.then(res => res.json())
|
|
676
|
-
.then(data => {
|
|
677
|
-
const engine = new Engine(data.registry, createEmptyState())
|
|
678
|
-
const snapshot = engine.newGame(data.config)
|
|
679
|
-
setGame({ engine, snapshot })
|
|
680
|
-
})
|
|
681
|
-
}, [])
|
|
682
|
-
|
|
683
|
-
if (!game) {
|
|
684
|
-
return <LoadingScreen />
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return (
|
|
688
|
-
<GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
|
|
689
|
-
<GameUI />
|
|
690
|
-
</GameProvider>
|
|
691
|
-
)
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function GameUI() {
|
|
695
|
-
const { snapshot, actions } = useGame()
|
|
696
|
-
|
|
697
|
-
return (
|
|
698
|
-
<div style={{ padding: '2rem', fontFamily: 'sans-serif', maxWidth: '800px', margin: '0 auto' }}>
|
|
699
|
-
<h1>{snapshot.location.name}</h1>
|
|
700
|
-
<p>{snapshot.location.description}</p>
|
|
701
|
-
|
|
702
|
-
{snapshot.dialogue && (
|
|
703
|
-
<div style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px', margin: '1rem 0' }}>
|
|
704
|
-
<strong>{snapshot.dialogue.speakerName}:</strong>
|
|
705
|
-
<p>{snapshot.dialogue.text}</p>
|
|
706
|
-
{snapshot.choices.map(choice => (
|
|
707
|
-
<button
|
|
708
|
-
key={choice.id}
|
|
709
|
-
onClick={() => actions.selectChoice(choice.id)}
|
|
710
|
-
style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
711
|
-
>
|
|
712
|
-
{choice.text}
|
|
713
|
-
</button>
|
|
714
|
-
))}
|
|
715
|
-
</div>
|
|
716
|
-
)}
|
|
717
|
-
|
|
718
|
-
{!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
|
|
719
|
-
<div>
|
|
720
|
-
<h2>Characters here</h2>
|
|
721
|
-
{snapshot.charactersHere.map(char => (
|
|
722
|
-
<button
|
|
723
|
-
key={char.id}
|
|
724
|
-
onClick={() => actions.talkTo(char.id)}
|
|
725
|
-
style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
726
|
-
>
|
|
727
|
-
Talk to {char.name}
|
|
728
|
-
</button>
|
|
729
|
-
))}
|
|
730
|
-
</div>
|
|
731
|
-
)}
|
|
732
|
-
</div>
|
|
733
|
-
)
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function createEmptyState(): GameState {
|
|
737
|
-
return {
|
|
738
|
-
currentLocation: '',
|
|
739
|
-
currentTime: { day: 1, hour: 0 },
|
|
740
|
-
flags: {},
|
|
741
|
-
variables: {},
|
|
742
|
-
inventory: [],
|
|
743
|
-
questProgress: {},
|
|
744
|
-
unlockedJournalEntries: [],
|
|
745
|
-
playerNotes: [],
|
|
746
|
-
dialogueState: null,
|
|
747
|
-
characterState: {},
|
|
748
|
-
itemLocations: {},
|
|
749
|
-
mapEnabled: true,
|
|
750
|
-
notifications: [],
|
|
751
|
-
pendingSounds: [],
|
|
752
|
-
pendingVideo: null,
|
|
753
|
-
currentLocale: 'en',
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
`, await m(s(e, "src/App.tsx"), d), await m(s(e, "src/index.css"), `body {
|
|
757
|
-
margin: 0;
|
|
758
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
759
|
-
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
760
|
-
sans-serif;
|
|
761
|
-
-webkit-font-smoothing: antialiased;
|
|
762
|
-
-moz-osx-font-smoothing: grayscale;
|
|
543
|
+
`, re = `{
|
|
544
|
+
"compilerOptions": {
|
|
545
|
+
"target": "ES2024",
|
|
546
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
547
|
+
"module": "ESNext",
|
|
548
|
+
"moduleResolution": "bundler",
|
|
549
|
+
"jsx": "react-jsx",
|
|
550
|
+
"strict": true,
|
|
551
|
+
"skipLibCheck": true,
|
|
552
|
+
"esModuleInterop": true,
|
|
553
|
+
"forceConsistentCasingInFileNames": true,
|
|
554
|
+
"resolveJsonModule": true,
|
|
555
|
+
"isolatedModules": true,
|
|
556
|
+
"noEmit": true
|
|
557
|
+
},
|
|
558
|
+
"include": ["src"]
|
|
763
559
|
}
|
|
764
|
-
|
|
765
|
-
startLocation: tavern
|
|
766
|
-
startTime:
|
|
767
|
-
day: 1
|
|
768
|
-
hour: 8
|
|
769
|
-
startFlags: {}
|
|
770
|
-
startVariables:
|
|
771
|
-
gold: 100
|
|
772
|
-
reputation: 0
|
|
773
|
-
_drinksBought: 0
|
|
774
|
-
startInventory: []
|
|
775
|
-
`), await m(s(e, "content/locations/tavern.yaml"), `id: tavern
|
|
776
|
-
name: "@location.tavern.name"
|
|
777
|
-
description: "@location.tavern.description"
|
|
778
|
-
banner: ""
|
|
779
|
-
music: ""
|
|
780
|
-
ambient: ""
|
|
781
|
-
`), await m(s(e, "content/locations/market.yaml"), `id: market
|
|
782
|
-
name: "@location.market.name"
|
|
783
|
-
description: "@location.market.description"
|
|
784
|
-
banner: ""
|
|
785
|
-
music: ""
|
|
786
|
-
ambient: ""
|
|
787
|
-
`), await m(s(e, "content/characters/bartender.yaml"), `id: bartender
|
|
560
|
+
`, se = `id: bartender
|
|
788
561
|
name: "@character.bartender.name"
|
|
789
562
|
biography: "@character.bartender.bio"
|
|
790
563
|
portrait: ""
|
|
791
564
|
location: tavern
|
|
792
565
|
dialogue: bartender_greeting
|
|
793
566
|
stats: {}
|
|
794
|
-
|
|
567
|
+
`, ie = `id: merchant
|
|
795
568
|
name: "@character.merchant.name"
|
|
796
569
|
biography: "@character.merchant.bio"
|
|
797
570
|
portrait: ""
|
|
798
571
|
location: market
|
|
799
572
|
dialogue: merchant_intro
|
|
800
573
|
stats: {}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
icon: ""
|
|
805
|
-
image: ""
|
|
806
|
-
location: tavern
|
|
807
|
-
stats: {}
|
|
808
|
-
`), await m(s(e, "content/maps/town.yaml"), `id: town
|
|
809
|
-
name: "@map.town.name"
|
|
810
|
-
image: ""
|
|
811
|
-
scale: 1
|
|
812
|
-
locations:
|
|
813
|
-
- id: tavern
|
|
814
|
-
x: 100
|
|
815
|
-
y: 200
|
|
816
|
-
- id: market
|
|
817
|
-
x: 300
|
|
818
|
-
y: 150
|
|
819
|
-
`), await m(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
|
|
820
|
-
name: "@quest.odd_jobs.name"
|
|
821
|
-
description: "@quest.odd_jobs.description"
|
|
822
|
-
stages:
|
|
823
|
-
- id: started
|
|
824
|
-
description: "@quest.odd_jobs.stage.started"
|
|
825
|
-
- id: talked_to_merchant
|
|
826
|
-
description: "@quest.odd_jobs.stage.talked_to_merchant"
|
|
827
|
-
- id: complete
|
|
828
|
-
description: "@quest.odd_jobs.stage.complete"
|
|
829
|
-
`), await m(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
|
|
830
|
-
title: "@journal.tavern_discovery.title"
|
|
831
|
-
text: "@journal.tavern_discovery.text"
|
|
832
|
-
category: places
|
|
833
|
-
`), await m(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
|
|
834
|
-
title: "@journal.odd_jobs_accepted.title"
|
|
835
|
-
text: "@journal.odd_jobs_accepted.text"
|
|
836
|
-
category: quests
|
|
837
|
-
`), await m(s(e, "content/journal/market_square.yaml"), `id: market_square
|
|
838
|
-
title: "@journal.market_square.title"
|
|
839
|
-
text: "@journal.market_square.text"
|
|
840
|
-
category: places
|
|
841
|
-
`), await m(s(e, "content/interludes/chapter_one.yaml"), `id: chapter_one
|
|
842
|
-
# Background image shown fullscreen during the interlude.
|
|
843
|
-
# Replace with your own image in assets/images/.
|
|
844
|
-
background: /assets/images/banners/tavern_banner.jpg
|
|
845
|
-
|
|
846
|
-
# Optional: decorative border/frame image overlaid on the background.
|
|
847
|
-
# banner: /assets/images/ui/chapter_frame.png
|
|
848
|
-
|
|
849
|
-
# Optional: music to play during this interlude.
|
|
850
|
-
# music: /assets/audio/music/chapter_theme.ogg
|
|
851
|
-
|
|
852
|
-
# The narrative text. Supports @localization.key or plain text.
|
|
853
|
-
text: |
|
|
854
|
-
Chapter One: A New Beginning
|
|
855
|
-
|
|
856
|
-
The road behind you stretches long and empty.
|
|
857
|
-
Ahead, the lights of town flicker through the evening mist.
|
|
858
|
-
|
|
859
|
-
You have heard the rumours. Strange things happening.
|
|
860
|
-
People going missing. Shadows that move wrong.
|
|
861
|
-
|
|
862
|
-
Someone has to look into it.
|
|
863
|
-
|
|
864
|
-
It might as well be you.
|
|
865
|
-
|
|
866
|
-
# Auto-trigger when the player enters the tavern for the first time.
|
|
867
|
-
# triggerConditions prevents re-triggering. effects runs when the interlude fires
|
|
868
|
-
# â setFlag here marks it seen so it won't show again on return visits.
|
|
869
|
-
triggerLocation: tavern
|
|
870
|
-
triggerConditions:
|
|
871
|
-
- type: notFlag
|
|
872
|
-
flag: seenChapterOne
|
|
873
|
-
effects:
|
|
874
|
-
- type: setFlag
|
|
875
|
-
flag: seenChapterOne
|
|
876
|
-
`), await m(s(e, "content/dialogues/tavern_intro.dlg"), `# This dialogue triggers automatically when the player enters the tavern.
|
|
877
|
-
# TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
|
|
878
|
-
# Use notFlag to make it a one-time intro.
|
|
879
|
-
|
|
880
|
-
TRIGGER tavern
|
|
881
|
-
REQUIRE notFlag seenTavernIntro
|
|
882
|
-
|
|
883
|
-
# Each NODE is a conversation point. The first NODE is always the start.
|
|
884
|
-
NODE start
|
|
885
|
-
# NARRATOR: has no speaker â used for scene-setting text.
|
|
886
|
-
# @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
|
|
887
|
-
# You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
|
|
888
|
-
NARRATOR: @narrator.tavern_intro
|
|
889
|
-
|
|
890
|
-
# Effects run immediately when this node is reached, before choices are shown.
|
|
891
|
-
SET flag seenTavernIntro
|
|
892
|
-
|
|
893
|
-
# CHOICE text can use a @key or "inline text".
|
|
894
|
-
# A choice with END dialogue is a terminal choice â no GOTO needed.
|
|
895
|
-
CHOICE @narrator.choice.look_around
|
|
896
|
-
END dialogue
|
|
897
|
-
END
|
|
898
|
-
`), await m(s(e, "content/dialogues/market_intro.dlg"), `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
|
|
899
|
-
|
|
900
|
-
TRIGGER market
|
|
901
|
-
REQUIRE notFlag seenMarketIntro
|
|
902
|
-
|
|
903
|
-
NODE start
|
|
904
|
-
NARRATOR: @narrator.market_intro
|
|
905
|
-
SET flag seenMarketIntro
|
|
906
|
-
|
|
907
|
-
# ADD journalEntry unlocks a journal entry for the player.
|
|
908
|
-
ADD journalEntry market_square
|
|
909
|
-
|
|
910
|
-
CHOICE @narrator.choice.look_around
|
|
911
|
-
END dialogue
|
|
912
|
-
END
|
|
913
|
-
`), await m(s(e, "content/dialogues/bartender_greeting.dlg"), `# This dialogue is triggered by clicking the bartender character.
|
|
914
|
-
# SPEAKER: lines set who's talking â matched to character ID (case-insensitive).
|
|
915
|
-
# Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
|
|
574
|
+
`, ce = `# This dialogue is triggered by clicking the bartender character.
|
|
575
|
+
# SPEAKER: lines set who's talking â matched to character ID (case-insensitive).
|
|
576
|
+
# Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
|
|
916
577
|
|
|
917
578
|
NODE start
|
|
918
579
|
BARTENDER: @bartender.greeting
|
|
@@ -987,6 +648,11 @@ NODE order_drink
|
|
|
987
648
|
GOTO after_drink
|
|
988
649
|
END
|
|
989
650
|
|
|
651
|
+
# Try to bluff a free drink â launches the skill check dialogue (demonstrates: START dialogue)
|
|
652
|
+
CHOICE @bartender.choice.try_bluff
|
|
653
|
+
START dialogue bluff_check
|
|
654
|
+
END
|
|
655
|
+
|
|
990
656
|
# Always available as an out
|
|
991
657
|
CHOICE @bartender.choice.too_rich
|
|
992
658
|
GOTO start
|
|
@@ -1052,7 +718,65 @@ NODE work_done
|
|
|
1052
718
|
NODE farewell
|
|
1053
719
|
BARTENDER: @bartender.farewell
|
|
1054
720
|
END dialogue
|
|
1055
|
-
|
|
721
|
+
`, le = `# Skill check example â demonstrates dice rolling and variable interpolation.
|
|
722
|
+
#
|
|
723
|
+
# ROLL <variable> <min> <max> â rolls a random integer and stores it in a variable.
|
|
724
|
+
# {varName} â in dialogue text, replaced with the variable's value.
|
|
725
|
+
# roll <min> <max> <threshold> â condition: rolls and returns true if result >= threshold.
|
|
726
|
+
#
|
|
727
|
+
# This dialogue is NOT auto-triggered â start it from another node with:
|
|
728
|
+
# START dialogue bluff_check
|
|
729
|
+
|
|
730
|
+
NODE start
|
|
731
|
+
# Roll a d20 and store it as "bluffRoll" before the player sees the result
|
|
732
|
+
ROLL bluffRoll 1 20
|
|
733
|
+
NARRATOR: @bluff.setup
|
|
734
|
+
|
|
735
|
+
CHOICE @bluff.choice.attempt
|
|
736
|
+
GOTO resolve
|
|
737
|
+
END
|
|
738
|
+
|
|
739
|
+
CHOICE @bluff.choice.back_down
|
|
740
|
+
NARRATOR: @bluff.backed_down
|
|
741
|
+
END dialogue
|
|
742
|
+
END
|
|
743
|
+
|
|
744
|
+
NODE resolve
|
|
745
|
+
# Show the roll result using {bluffRoll} interpolation in the locale string
|
|
746
|
+
NARRATOR: @bluff.rolled
|
|
747
|
+
|
|
748
|
+
# Branch on whether the stored variable passes a threshold (15+)
|
|
749
|
+
IF variableGreaterThan bluffRoll 14
|
|
750
|
+
GOTO success
|
|
751
|
+
END
|
|
752
|
+
|
|
753
|
+
GOTO failure
|
|
754
|
+
|
|
755
|
+
NODE success
|
|
756
|
+
NARRATOR: @bluff.success
|
|
757
|
+
ADD relationship bartender 2
|
|
758
|
+
SET flag bluffedMarcus
|
|
759
|
+
END dialogue
|
|
760
|
+
|
|
761
|
+
NODE failure
|
|
762
|
+
NARRATOR: @bluff.failure
|
|
763
|
+
END dialogue
|
|
764
|
+
`, de = `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
|
|
765
|
+
|
|
766
|
+
TRIGGER market
|
|
767
|
+
REQUIRE notFlag seenMarketIntro
|
|
768
|
+
|
|
769
|
+
NODE start
|
|
770
|
+
NARRATOR: @narrator.market_intro
|
|
771
|
+
SET flag seenMarketIntro
|
|
772
|
+
|
|
773
|
+
# ADD journalEntry unlocks a journal entry for the player.
|
|
774
|
+
ADD journalEntry market_square
|
|
775
|
+
|
|
776
|
+
CHOICE @narrator.choice.look_around
|
|
777
|
+
END dialogue
|
|
778
|
+
END
|
|
779
|
+
`, ue = `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
|
|
1056
780
|
# The quest choices here demonstrate multi-stage quest gating with questAtStage.
|
|
1057
781
|
|
|
1058
782
|
NODE start
|
|
@@ -1155,50 +879,94 @@ NODE about_market
|
|
|
1155
879
|
NODE farewell
|
|
1156
880
|
MERCHANT: @merchant.farewell
|
|
1157
881
|
END dialogue
|
|
1158
|
-
|
|
1159
|
-
#
|
|
1160
|
-
#
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
# This dialogue is NOT auto-triggered â start it from another node with:
|
|
1165
|
-
# START dialogue bluff_check
|
|
882
|
+
`, he = `# This dialogue triggers automatically when the player enters the tavern.
|
|
883
|
+
# TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
|
|
884
|
+
# Use notFlag to make it a one-time intro.
|
|
885
|
+
|
|
886
|
+
TRIGGER tavern
|
|
887
|
+
REQUIRE notFlag seenTavernIntro
|
|
1166
888
|
|
|
889
|
+
# Each NODE is a conversation point. The first NODE is always the start.
|
|
1167
890
|
NODE start
|
|
1168
|
-
#
|
|
1169
|
-
|
|
1170
|
-
NARRATOR:
|
|
891
|
+
# NARRATOR: has no speaker â used for scene-setting text.
|
|
892
|
+
# @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
|
|
893
|
+
# You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
|
|
894
|
+
NARRATOR: @narrator.tavern_intro
|
|
1171
895
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
END
|
|
896
|
+
# Effects run immediately when this node is reached, before choices are shown.
|
|
897
|
+
SET flag seenTavernIntro
|
|
1175
898
|
|
|
1176
|
-
CHOICE @
|
|
1177
|
-
|
|
899
|
+
# CHOICE text can use a @key or "inline text".
|
|
900
|
+
# A choice with END dialogue is a terminal choice â no GOTO needed.
|
|
901
|
+
CHOICE @narrator.choice.look_around
|
|
1178
902
|
END dialogue
|
|
1179
903
|
END
|
|
904
|
+
`, me = `# Game Configuration
|
|
905
|
+
startLocation: tavern
|
|
906
|
+
startTime:
|
|
907
|
+
day: 1
|
|
908
|
+
hour: 8
|
|
909
|
+
startFlags: {}
|
|
910
|
+
startVariables:
|
|
911
|
+
gold: 100
|
|
912
|
+
reputation: 0
|
|
913
|
+
_drinksBought: 0
|
|
914
|
+
startInventory: []
|
|
915
|
+
`, ge = `id: chapter_one
|
|
916
|
+
# Background image shown fullscreen during the interlude.
|
|
917
|
+
# Replace with your own image in assets/images/.
|
|
918
|
+
background: /assets/images/banners/tavern_banner.jpg
|
|
1180
919
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
NARRATOR: @bluff.rolled
|
|
920
|
+
# Optional: decorative border/frame image overlaid on the background.
|
|
921
|
+
# banner: /assets/images/ui/chapter_frame.png
|
|
1184
922
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
GOTO success
|
|
1188
|
-
END
|
|
923
|
+
# Optional: music to play during this interlude.
|
|
924
|
+
# music: /assets/audio/music/chapter_theme.ogg
|
|
1189
925
|
|
|
1190
|
-
|
|
926
|
+
# The narrative text. Supports @localization.key or plain text.
|
|
927
|
+
text: |
|
|
928
|
+
Chapter One: A New Beginning
|
|
1191
929
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
ADD relationship bartender 2
|
|
1195
|
-
SET flag bluffedMarcus
|
|
1196
|
-
END dialogue
|
|
930
|
+
The road behind you stretches long and empty.
|
|
931
|
+
Ahead, the lights of town flicker through the evening mist.
|
|
1197
932
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
933
|
+
You have heard the rumours. Strange things happening.
|
|
934
|
+
People going missing. Shadows that move wrong.
|
|
935
|
+
|
|
936
|
+
Someone has to look into it.
|
|
937
|
+
|
|
938
|
+
It might as well be you.
|
|
939
|
+
|
|
940
|
+
# Auto-trigger when the player enters the tavern for the first time.
|
|
941
|
+
# triggerConditions prevents re-triggering. effects runs when the interlude fires
|
|
942
|
+
# â setFlag here marks it seen so it won't show again on return visits.
|
|
943
|
+
triggerLocation: tavern
|
|
944
|
+
triggerConditions:
|
|
945
|
+
- type: notFlag
|
|
946
|
+
flag: seenChapterOne
|
|
947
|
+
effects:
|
|
948
|
+
- type: setFlag
|
|
949
|
+
flag: seenChapterOne
|
|
950
|
+
`, fe = `id: old_coin
|
|
951
|
+
name: "@item.old_coin.name"
|
|
952
|
+
description: "@item.old_coin.description"
|
|
953
|
+
icon: ""
|
|
954
|
+
image: ""
|
|
955
|
+
location: tavern
|
|
956
|
+
stats: {}
|
|
957
|
+
`, pe = `id: market_square
|
|
958
|
+
title: "@journal.market_square.title"
|
|
959
|
+
text: "@journal.market_square.text"
|
|
960
|
+
category: places
|
|
961
|
+
`, ye = `id: odd_jobs_accepted
|
|
962
|
+
title: "@journal.odd_jobs_accepted.title"
|
|
963
|
+
text: "@journal.odd_jobs_accepted.text"
|
|
964
|
+
category: quests
|
|
965
|
+
`, be = `id: tavern_discovery
|
|
966
|
+
title: "@journal.tavern_discovery.title"
|
|
967
|
+
text: "@journal.tavern_discovery.text"
|
|
968
|
+
category: places
|
|
969
|
+
`, _e = `# ===================
|
|
1202
970
|
# Narrator Intros
|
|
1203
971
|
# ===================
|
|
1204
972
|
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."
|
|
@@ -1267,6 +1035,7 @@ bartender.choice.nevermind: "Never mind, just passing through."
|
|
|
1267
1035
|
bartender.choice.tell_me_more: "Tell me more about that."
|
|
1268
1036
|
bartender.choice.interesting: "Interesting. I'll keep that in mind."
|
|
1269
1037
|
bartender.choice.sure_pay: "Sure, here's five gold."
|
|
1038
|
+
bartender.choice.try_bluff: "Try to talk your way to a free drink."
|
|
1270
1039
|
bartender.choice.too_rich: "On second thought, I'll pass."
|
|
1271
1040
|
bartender.choice.back_to_chat: "So, what else?"
|
|
1272
1041
|
bartender.choice.accept_work: "Sure, I could use the coin."
|
|
@@ -1335,26 +1104,298 @@ bluff.choice.attempt: "Try to bluff him."
|
|
|
1335
1104
|
bluff.choice.back_down: "Actually, never mind."
|
|
1336
1105
|
bluff.backed_down: "Some fights aren't worth picking."
|
|
1337
1106
|
bluff.rolled: "You spin the tale with {bluffRoll} on your roll â and Marcus listens carefully."
|
|
1338
|
-
bluff.success: "The story lands perfectly. Marcus laughs and slides a free drink across the bar.
|
|
1339
|
-
bluff.failure: "Marcus raises an eyebrow
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
.
|
|
1343
|
-
|
|
1344
|
-
|
|
1107
|
+
bluff.success: "The story lands perfectly. Marcus laughs and slides a free drink across the bar. Not bad at all, he admits."
|
|
1108
|
+
bluff.failure: "Marcus raises an eyebrow, entirely unconvinced. That will be five gold, he says flatly."
|
|
1109
|
+
`, we = `id: market
|
|
1110
|
+
name: "@location.market.name"
|
|
1111
|
+
description: "@location.market.description"
|
|
1112
|
+
banner: ""
|
|
1113
|
+
music: ""
|
|
1114
|
+
ambient: ""
|
|
1115
|
+
`, ke = `id: tavern
|
|
1116
|
+
name: "@location.tavern.name"
|
|
1117
|
+
description: "@location.tavern.description"
|
|
1118
|
+
banner: ""
|
|
1119
|
+
music: ""
|
|
1120
|
+
ambient: ""
|
|
1121
|
+
`, Ee = `id: town
|
|
1122
|
+
name: "@map.town.name"
|
|
1123
|
+
image: ""
|
|
1124
|
+
scale: 1
|
|
1125
|
+
locations:
|
|
1126
|
+
- id: tavern
|
|
1127
|
+
x: 100
|
|
1128
|
+
y: 200
|
|
1129
|
+
- id: market
|
|
1130
|
+
x: 300
|
|
1131
|
+
y: 150
|
|
1132
|
+
`, ve = `id: odd_jobs
|
|
1133
|
+
name: "@quest.odd_jobs.name"
|
|
1134
|
+
description: "@quest.odd_jobs.description"
|
|
1135
|
+
stages:
|
|
1136
|
+
- id: started
|
|
1137
|
+
description: "@quest.odd_jobs.stage.started"
|
|
1138
|
+
- id: talked_to_merchant
|
|
1139
|
+
description: "@quest.odd_jobs.stage.talked_to_merchant"
|
|
1140
|
+
- id: complete
|
|
1141
|
+
description: "@quest.odd_jobs.stage.complete"
|
|
1142
|
+
`, Oe = `import { useEffect, useState } from 'react'
|
|
1143
|
+
import { Engine } from '@doodle-engine/core'
|
|
1144
|
+
import type { GameState, Snapshot } from '@doodle-engine/core'
|
|
1145
|
+
import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
|
|
1146
|
+
|
|
1147
|
+
export function App() {
|
|
1148
|
+
const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
|
|
1149
|
+
|
|
1150
|
+
useEffect(() => {
|
|
1151
|
+
fetch('/api/content')
|
|
1152
|
+
.then(res => res.json())
|
|
1153
|
+
.then(data => {
|
|
1154
|
+
const engine = new Engine(data.registry, createEmptyState())
|
|
1155
|
+
const snapshot = engine.newGame(data.config)
|
|
1156
|
+
setGame({ engine, snapshot })
|
|
1157
|
+
})
|
|
1158
|
+
}, [])
|
|
1159
|
+
|
|
1160
|
+
if (!game) {
|
|
1161
|
+
return <LoadingScreen />
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return (
|
|
1165
|
+
<GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
|
|
1166
|
+
<GameUI />
|
|
1167
|
+
</GameProvider>
|
|
1168
|
+
)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function GameUI() {
|
|
1172
|
+
const { snapshot, actions } = useGame()
|
|
1173
|
+
|
|
1174
|
+
return (
|
|
1175
|
+
<div style={{ padding: '2rem', fontFamily: 'sans-serif', maxWidth: '800px', margin: '0 auto' }}>
|
|
1176
|
+
<h1>{snapshot.location.name}</h1>
|
|
1177
|
+
<p>{snapshot.location.description}</p>
|
|
1178
|
+
|
|
1179
|
+
{snapshot.dialogue && (
|
|
1180
|
+
<div style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px', margin: '1rem 0' }}>
|
|
1181
|
+
<strong>{snapshot.dialogue.speakerName}:</strong>
|
|
1182
|
+
<p>{snapshot.dialogue.text}</p>
|
|
1183
|
+
{snapshot.choices.map(choice => (
|
|
1184
|
+
<button
|
|
1185
|
+
key={choice.id}
|
|
1186
|
+
onClick={() => actions.selectChoice(choice.id)}
|
|
1187
|
+
style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
1188
|
+
>
|
|
1189
|
+
{choice.text}
|
|
1190
|
+
</button>
|
|
1191
|
+
))}
|
|
1192
|
+
</div>
|
|
1193
|
+
)}
|
|
1194
|
+
|
|
1195
|
+
{!snapshot.dialogue && snapshot.charactersHere.length > 0 && (
|
|
1196
|
+
<div>
|
|
1197
|
+
<h2>Characters here</h2>
|
|
1198
|
+
{snapshot.charactersHere.map(char => (
|
|
1199
|
+
<button
|
|
1200
|
+
key={char.id}
|
|
1201
|
+
onClick={() => actions.talkTo(char.id)}
|
|
1202
|
+
style={{ display: 'block', margin: '0.5rem 0', padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
1203
|
+
>
|
|
1204
|
+
Talk to {char.name}
|
|
1205
|
+
</button>
|
|
1206
|
+
))}
|
|
1207
|
+
</div>
|
|
1208
|
+
)}
|
|
1209
|
+
</div>
|
|
1210
|
+
)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function createEmptyState(): GameState {
|
|
1214
|
+
return {
|
|
1215
|
+
currentLocation: '',
|
|
1216
|
+
currentTime: { day: 1, hour: 0 },
|
|
1217
|
+
flags: {},
|
|
1218
|
+
variables: {},
|
|
1219
|
+
inventory: [],
|
|
1220
|
+
questProgress: {},
|
|
1221
|
+
unlockedJournalEntries: [],
|
|
1222
|
+
playerNotes: [],
|
|
1223
|
+
dialogueState: null,
|
|
1224
|
+
characterState: {},
|
|
1225
|
+
itemLocations: {},
|
|
1226
|
+
mapEnabled: true,
|
|
1227
|
+
notifications: [],
|
|
1228
|
+
pendingSounds: [],
|
|
1229
|
+
pendingVideo: null,
|
|
1230
|
+
pendingInterlude: null,
|
|
1231
|
+
currentLocale: 'en',
|
|
1232
|
+
}
|
|
1345
1233
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1234
|
+
`, De = `import { useEffect, useState } from 'react'
|
|
1235
|
+
import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
|
|
1236
|
+
import { GameShell, LoadingScreen } from '@doodle-engine/react'
|
|
1237
|
+
|
|
1238
|
+
export function App() {
|
|
1239
|
+
const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
|
|
1240
|
+
|
|
1241
|
+
useEffect(() => {
|
|
1242
|
+
fetch('/api/content')
|
|
1243
|
+
.then(res => res.json())
|
|
1244
|
+
.then(data => setContent({ registry: data.registry, config: data.config }))
|
|
1245
|
+
}, [])
|
|
1246
|
+
|
|
1247
|
+
if (!content) {
|
|
1248
|
+
return <LoadingScreen />
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return (
|
|
1252
|
+
<GameShell
|
|
1253
|
+
registry={content.registry}
|
|
1254
|
+
config={content.config}
|
|
1255
|
+
title="My Doodle Game"
|
|
1256
|
+
subtitle="A text-based adventure"
|
|
1257
|
+
splashDuration={2000}
|
|
1258
|
+
availableLocales={[{ code: 'en', label: 'English' }]}
|
|
1259
|
+
devTools={import.meta.env.DEV}
|
|
1260
|
+
/>
|
|
1261
|
+
)
|
|
1262
|
+
}
|
|
1263
|
+
`, Te = `body {
|
|
1264
|
+
margin: 0;
|
|
1265
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
1266
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
1267
|
+
sans-serif;
|
|
1268
|
+
-webkit-font-smoothing: antialiased;
|
|
1269
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1270
|
+
}
|
|
1271
|
+
`, Ie = `import { StrictMode } from 'react'
|
|
1272
|
+
import { createRoot } from 'react-dom/client'
|
|
1273
|
+
import { App } from './App'
|
|
1274
|
+
import './index.css'
|
|
1275
|
+
|
|
1276
|
+
createRoot(document.getElementById('root')!).render(
|
|
1277
|
+
<StrictMode>
|
|
1278
|
+
<App />
|
|
1279
|
+
</StrictMode>,
|
|
1280
|
+
)
|
|
1281
|
+
`, O = "ðū", Ne = "ð", x = "ðĶī", Ce = "âĻ", q = "ð", T = "â
", Re = "ð", S = /* @__PURE__ */ Object.assign({
|
|
1282
|
+
"./templates/_root/_gitignore": oe,
|
|
1283
|
+
"./templates/_root/index.html": ae,
|
|
1284
|
+
"./templates/_root/tsconfig.json": re,
|
|
1285
|
+
"./templates/content/characters/bartender.yaml": se,
|
|
1286
|
+
"./templates/content/characters/merchant.yaml": ie,
|
|
1287
|
+
"./templates/content/dialogues/bartender_greeting.dlg": ce,
|
|
1288
|
+
"./templates/content/dialogues/bluff_check.dlg": le,
|
|
1289
|
+
"./templates/content/dialogues/market_intro.dlg": de,
|
|
1290
|
+
"./templates/content/dialogues/merchant_intro.dlg": ue,
|
|
1291
|
+
"./templates/content/dialogues/tavern_intro.dlg": he,
|
|
1292
|
+
"./templates/content/game.yaml": me,
|
|
1293
|
+
"./templates/content/interludes/chapter_one.yaml": ge,
|
|
1294
|
+
"./templates/content/items/old_coin.yaml": fe,
|
|
1295
|
+
"./templates/content/journal/market_square.yaml": pe,
|
|
1296
|
+
"./templates/content/journal/odd_jobs_accepted.yaml": ye,
|
|
1297
|
+
"./templates/content/journal/tavern_discovery.yaml": be,
|
|
1298
|
+
"./templates/content/locales/en.yaml": _e,
|
|
1299
|
+
"./templates/content/locations/market.yaml": we,
|
|
1300
|
+
"./templates/content/locations/tavern.yaml": ke,
|
|
1301
|
+
"./templates/content/maps/town.yaml": Ee,
|
|
1302
|
+
"./templates/content/quests/odd_jobs.yaml": ve,
|
|
1303
|
+
"./templates/src/App.custom.tsx": Oe,
|
|
1304
|
+
"./templates/src/App.default.tsx": De,
|
|
1305
|
+
"./templates/src/index.css": Te,
|
|
1306
|
+
"./templates/src/main.tsx": Ie
|
|
1350
1307
|
});
|
|
1351
|
-
|
|
1352
|
-
|
|
1308
|
+
function Ae(n) {
|
|
1309
|
+
const o = n.replace("./templates/", "");
|
|
1310
|
+
if (o === "src/App.default.tsx" || o === "src/App.custom.tsx") return null;
|
|
1311
|
+
if (o.startsWith("_root/")) {
|
|
1312
|
+
const r = o.slice(6);
|
|
1313
|
+
return r.startsWith("_") ? "." + r.slice(1) : r;
|
|
1314
|
+
}
|
|
1315
|
+
return o;
|
|
1316
|
+
}
|
|
1317
|
+
async function Se(n) {
|
|
1318
|
+
const o = d(process.cwd(), n);
|
|
1319
|
+
console.log(""), console.log(i.bold.magenta(` ${O} Doodle Engine ${O}`)), console.log(i.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${Ne} Creating new game: ${i.bold.cyan(n)}`), console.log("");
|
|
1320
|
+
const { useDefaultRenderer: r } = await P({
|
|
1321
|
+
type: "confirm",
|
|
1322
|
+
name: "useDefaultRenderer",
|
|
1323
|
+
message: "Use default renderer?",
|
|
1324
|
+
initial: !0
|
|
1325
|
+
});
|
|
1326
|
+
r === void 0 && (console.log(i.yellow(`
|
|
1327
|
+
${x} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await je(o, n, r), console.log(""), console.log(i.bold.green(` ${T} Project created successfully!`)), console.log(""), console.log(i.dim(` ${q} ${o}`)), console.log(""), console.log(i.bold(" Next steps:")), console.log(i.cyan(` cd ${n}`)), console.log(i.cyan(" npm install ") + i.dim("# or: yarn install / pnpm install")), console.log(i.cyan(" npm run dev ") + i.dim("# or: yarn dev / pnpm dev")), console.log(""), console.log(i.dim(` ${Re} Happy game making! ${O}`)), console.log("");
|
|
1328
|
+
}
|
|
1329
|
+
async function je(n, o, r) {
|
|
1330
|
+
const a = [
|
|
1331
|
+
"content/locations",
|
|
1332
|
+
"content/characters",
|
|
1333
|
+
"content/items",
|
|
1334
|
+
"content/dialogues",
|
|
1335
|
+
"content/quests",
|
|
1336
|
+
"content/journal",
|
|
1337
|
+
"content/interludes",
|
|
1338
|
+
"content/locales",
|
|
1339
|
+
"content/maps",
|
|
1340
|
+
"assets/images/banners",
|
|
1341
|
+
"assets/images/portraits",
|
|
1342
|
+
"assets/images/items",
|
|
1343
|
+
"assets/images/maps",
|
|
1344
|
+
"assets/audio/music",
|
|
1345
|
+
"assets/audio/sfx",
|
|
1346
|
+
"assets/audio/voice",
|
|
1347
|
+
"src"
|
|
1348
|
+
];
|
|
1349
|
+
console.log(` ${q} ${i.bold("Creating directories...")}`);
|
|
1350
|
+
for (const e of a)
|
|
1351
|
+
await D(d(n, e), { recursive: !0 });
|
|
1352
|
+
console.log(i.green(` ${T} Directories created`)), console.log("");
|
|
1353
|
+
const t = {
|
|
1354
|
+
name: o,
|
|
1355
|
+
version: "0.1.0",
|
|
1356
|
+
type: "module",
|
|
1357
|
+
scripts: {
|
|
1358
|
+
dev: "doodle dev",
|
|
1359
|
+
build: "doodle build",
|
|
1360
|
+
preview: "vite preview"
|
|
1361
|
+
},
|
|
1362
|
+
dependencies: {
|
|
1363
|
+
"@doodle-engine/core": "latest",
|
|
1364
|
+
"@doodle-engine/react": "latest",
|
|
1365
|
+
react: "^19.0.0",
|
|
1366
|
+
"react-dom": "^19.0.0"
|
|
1367
|
+
},
|
|
1368
|
+
devDependencies: {
|
|
1369
|
+
"@doodle-engine/cli": "latest",
|
|
1370
|
+
"@types/react": "^19.0.0",
|
|
1371
|
+
"@types/react-dom": "^19.0.0",
|
|
1372
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
1373
|
+
typescript: "^5.7.0",
|
|
1374
|
+
vite: "^6.0.0"
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
console.log(` ${Ce} ${i.bold("Writing project files...")}`), await k(d(n, "package.json"), JSON.stringify(t, null, 2));
|
|
1378
|
+
for (const [e, c] of Object.entries(S)) {
|
|
1379
|
+
const l = Ae(e);
|
|
1380
|
+
if (l === null) continue;
|
|
1381
|
+
const u = d(n, l);
|
|
1382
|
+
await D(F(u), { recursive: !0 }), await k(u, c);
|
|
1383
|
+
}
|
|
1384
|
+
const s = r ? "./templates/src/App.default.tsx" : "./templates/src/App.custom.tsx";
|
|
1385
|
+
await k(d(n, "src/App.tsx"), S[s]), console.log(i.green(` ${T} Source files created`)), console.log(""), console.log(` ${x} ${i.bold("Starter content written")}`), console.log(""), console.log(i.dim(" Content includes:")), console.log(i.dim(" 2 locations (tavern, market)")), console.log(i.dim(" 2 characters (bartender, merchant)")), console.log(i.dim(" 1 item (old coin)")), console.log(i.dim(" 1 map (town with 2 locations)")), console.log(i.dim(" 1 quest (odd jobs, 3 stages)")), console.log(i.dim(" 3 journal entries")), console.log(i.dim(" 1 interlude (chapter one, auto-triggers at tavern)")), console.log(i.dim(" 5 dialogues (2 narrator intros, 2 NPC conversations, 1 skill check)")), console.log(i.dim(" English locale with all strings"));
|
|
1386
|
+
}
|
|
1387
|
+
const _ = new G();
|
|
1388
|
+
_.name("doodle").description(i.magenta("ðū Doodle Engine") + i.dim(" â Narrative RPG development tools")).version("0.0.1");
|
|
1389
|
+
_.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (n) => {
|
|
1390
|
+
await Se(n);
|
|
1353
1391
|
});
|
|
1354
|
-
|
|
1355
|
-
await
|
|
1392
|
+
_.command("dev").description("Start development server with hot reload").action(async () => {
|
|
1393
|
+
await K();
|
|
1356
1394
|
});
|
|
1357
|
-
|
|
1395
|
+
_.command("build").description("Build game for production").action(async () => {
|
|
1358
1396
|
await Z();
|
|
1359
1397
|
});
|
|
1360
|
-
|
|
1398
|
+
_.command("validate").description("Validate game content").action(async () => {
|
|
1399
|
+
await ne();
|
|
1400
|
+
});
|
|
1401
|
+
_.parse();
|