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