@doodle-engine/cli 0.0.8 → 0.0.10
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 +412 -441
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/dev.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,358 +1,258 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command as
|
|
3
|
-
import { crayon as
|
|
4
|
-
import { createServer as
|
|
5
|
-
import
|
|
6
|
-
import { watch as
|
|
7
|
-
import { readdir as y, readFile as
|
|
8
|
-
import { join as s, extname as
|
|
2
|
+
import { Command as G } from "commander";
|
|
3
|
+
import { crayon as c } from "crayon.js";
|
|
4
|
+
import { createServer as j, build as $ } from "vite";
|
|
5
|
+
import A from "@vitejs/plugin-react";
|
|
6
|
+
import { watch as H } from "chokidar";
|
|
7
|
+
import { readdir as y, readFile as p, mkdir as x, writeFile as m } from "fs/promises";
|
|
8
|
+
import { join as s, extname as f, relative as w } from "path";
|
|
9
9
|
import { parse as b } from "yaml";
|
|
10
|
-
import { parseDialogue as
|
|
11
|
-
import
|
|
12
|
-
function
|
|
13
|
-
const
|
|
14
|
-
for (const
|
|
15
|
-
const
|
|
16
|
-
|
|
10
|
+
import { parseDialogue as v } from "@doodle-engine/core";
|
|
11
|
+
import M from "prompts";
|
|
12
|
+
function I(e, n) {
|
|
13
|
+
const i = [];
|
|
14
|
+
for (const a of Object.values(e.dialogues)) {
|
|
15
|
+
const o = n.get(a.id) || `dialogue:${a.id}`;
|
|
16
|
+
i.push(...F(a, o));
|
|
17
17
|
}
|
|
18
|
-
for (const
|
|
19
|
-
if (
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
file:
|
|
23
|
-
message: `Character "${
|
|
24
|
-
suggestion: `Create dialogue "${
|
|
18
|
+
for (const a of Object.values(e.characters))
|
|
19
|
+
if (a.dialogue && !e.dialogues[a.dialogue]) {
|
|
20
|
+
const o = n.get(a.id) || `character:${a.id}`;
|
|
21
|
+
i.push({
|
|
22
|
+
file: o,
|
|
23
|
+
message: `Character "${a.id}" references non-existent dialogue "${a.dialogue}"`,
|
|
24
|
+
suggestion: `Create dialogue "${a.dialogue}" or fix the reference`
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
return
|
|
27
|
+
return i.push(...J(e, n)), i;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
for (const
|
|
32
|
-
|
|
33
|
-
file:
|
|
34
|
-
message: `Duplicate node ID "${
|
|
29
|
+
function F(e, n) {
|
|
30
|
+
const i = [], a = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const o of e.nodes)
|
|
32
|
+
a.has(o.id) && i.push({
|
|
33
|
+
file: n,
|
|
34
|
+
message: `Duplicate node ID "${o.id}"`,
|
|
35
35
|
suggestion: "Node IDs must be unique within a dialogue"
|
|
36
|
-
}),
|
|
37
|
-
|
|
38
|
-
file:
|
|
36
|
+
}), a.add(o.id);
|
|
37
|
+
a.has(e.startNode) || i.push({
|
|
38
|
+
file: n,
|
|
39
39
|
message: `Start node "${e.startNode}" not found`,
|
|
40
40
|
suggestion: `Add a NODE ${e.startNode} or fix the startNode reference`
|
|
41
41
|
});
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
return
|
|
42
|
+
for (const o of e.nodes)
|
|
43
|
+
i.push(...B(o, a, n));
|
|
44
|
+
return i;
|
|
45
45
|
}
|
|
46
|
-
function
|
|
47
|
-
const
|
|
48
|
-
if (e.next && !
|
|
49
|
-
file:
|
|
46
|
+
function B(e, n, i) {
|
|
47
|
+
const a = [];
|
|
48
|
+
if (e.next && !n.has(e.next) && a.push({
|
|
49
|
+
file: i,
|
|
50
50
|
message: `Node "${e.id}" GOTO "${e.next}" points to non-existent node`,
|
|
51
51
|
suggestion: `Add NODE ${e.next} or fix the GOTO target`
|
|
52
52
|
}), e.conditionalNext)
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
file:
|
|
56
|
-
message: `Node "${e.id}" IF block GOTO "${
|
|
57
|
-
suggestion: `Add NODE ${
|
|
58
|
-
}),
|
|
59
|
-
for (const
|
|
60
|
-
if (o.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
for (const o of e.conditionalNext)
|
|
54
|
+
n.has(o.next) || a.push({
|
|
55
|
+
file: i,
|
|
56
|
+
message: `Node "${e.id}" IF block GOTO "${o.next}" points to non-existent node`,
|
|
57
|
+
suggestion: `Add NODE ${o.next} or fix the GOTO target`
|
|
58
|
+
}), a.push(...O(o.condition, e.id, i));
|
|
59
|
+
for (const o of e.choices) {
|
|
60
|
+
if (!o.effects?.some(
|
|
61
|
+
(t) => t.type === "endDialogue" || t.type === "goToLocation"
|
|
62
|
+
) && !n.has(o.next) && a.push({
|
|
63
|
+
file: i,
|
|
64
|
+
message: `Node "${e.id}" choice "${o.id}" GOTO "${o.next}" points to non-existent node`,
|
|
65
|
+
suggestion: `Add NODE ${o.next} or fix the GOTO target`
|
|
66
|
+
}), o.conditions)
|
|
67
|
+
for (const t of o.conditions)
|
|
68
|
+
a.push(...O(t, e.id, i));
|
|
69
|
+
if (o.effects)
|
|
70
|
+
for (const t of o.effects)
|
|
71
|
+
a.push(...C(t, e.id, i));
|
|
70
72
|
}
|
|
71
73
|
if (e.conditions)
|
|
72
|
-
for (const
|
|
73
|
-
|
|
74
|
+
for (const o of e.conditions)
|
|
75
|
+
a.push(...O(o, e.id, i));
|
|
74
76
|
if (e.effects)
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
return
|
|
77
|
+
for (const o of e.effects)
|
|
78
|
+
a.push(...C(o, e.id, i));
|
|
79
|
+
return a;
|
|
78
80
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const L = {
|
|
82
|
+
hasFlag: ["flag"],
|
|
83
|
+
notFlag: ["flag"],
|
|
84
|
+
hasItem: ["itemId"],
|
|
85
|
+
notItem: ["itemId"],
|
|
86
|
+
variableEquals: ["variable", "value"],
|
|
87
|
+
variableGreaterThan: ["variable", "value"],
|
|
88
|
+
variableLessThan: ["variable", "value"],
|
|
89
|
+
questAtStage: ["questId", "stageId"],
|
|
90
|
+
atLocation: ["locationId"],
|
|
91
|
+
characterAt: ["characterId", "locationId"],
|
|
92
|
+
characterInParty: ["characterId"],
|
|
93
|
+
relationshipAbove: ["characterId", "value"],
|
|
94
|
+
relationshipBelow: ["characterId", "value"],
|
|
95
|
+
itemAt: ["itemId", "locationId"]
|
|
96
|
+
}, U = {
|
|
97
|
+
setFlag: ["flag"],
|
|
98
|
+
clearFlag: ["flag"],
|
|
99
|
+
setVariable: ["variable", "value"],
|
|
100
|
+
addVariable: ["variable", "value"],
|
|
101
|
+
addItem: ["itemId"],
|
|
102
|
+
removeItem: ["itemId"],
|
|
103
|
+
moveItem: ["itemId", "locationId"],
|
|
104
|
+
goToLocation: ["locationId"],
|
|
105
|
+
advanceTime: ["hours"],
|
|
106
|
+
setQuestStage: ["questId", "stageId"],
|
|
107
|
+
addJournalEntry: ["entryId"],
|
|
108
|
+
startDialogue: ["dialogueId"],
|
|
109
|
+
endDialogue: [],
|
|
110
|
+
setCharacterLocation: ["characterId", "locationId"],
|
|
111
|
+
addToParty: ["characterId"],
|
|
112
|
+
removeFromParty: ["characterId"],
|
|
113
|
+
setRelationship: ["characterId", "value"],
|
|
114
|
+
addRelationship: ["characterId", "value"],
|
|
115
|
+
setCharacterStat: ["characterId", "stat", "value"],
|
|
116
|
+
addCharacterStat: ["characterId", "stat", "value"],
|
|
117
|
+
setMapEnabled: ["enabled"],
|
|
118
|
+
playMusic: ["track"],
|
|
119
|
+
playSound: ["sound"],
|
|
120
|
+
notify: ["message"],
|
|
121
|
+
playVideo: ["file"],
|
|
122
|
+
showInterlude: ["interludeId"]
|
|
123
|
+
};
|
|
124
|
+
function O(e, n, i) {
|
|
125
|
+
const a = [];
|
|
81
126
|
if (!e.type)
|
|
82
|
-
return
|
|
83
|
-
file:
|
|
84
|
-
message: `Node "${
|
|
85
|
-
}),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
break;
|
|
101
|
-
case "questAtStage":
|
|
102
|
-
e.quest || n.push({
|
|
103
|
-
file: a,
|
|
104
|
-
message: `Node "${o}" condition "questAtStage" missing required "quest" argument`
|
|
105
|
-
}), e.stage || n.push({
|
|
106
|
-
file: a,
|
|
107
|
-
message: `Node "${o}" condition "questAtStage" missing required "stage" argument`
|
|
108
|
-
});
|
|
109
|
-
break;
|
|
110
|
-
case "variableEquals":
|
|
111
|
-
case "variableGreaterThan":
|
|
112
|
-
case "variableLessThan":
|
|
113
|
-
e.variable || n.push({
|
|
114
|
-
file: a,
|
|
115
|
-
message: `Node "${o}" condition "${e.type}" missing required "variable" argument`
|
|
116
|
-
}), e.value === void 0 && n.push({
|
|
117
|
-
file: a,
|
|
118
|
-
message: `Node "${o}" condition "${e.type}" missing required "value" argument`
|
|
119
|
-
});
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
return n;
|
|
127
|
+
return a.push({
|
|
128
|
+
file: i,
|
|
129
|
+
message: `Node "${n}" has condition with missing type`
|
|
130
|
+
}), a;
|
|
131
|
+
if (e.type === "timeIs")
|
|
132
|
+
return e.hour === void 0 && e.day === void 0 && a.push({
|
|
133
|
+
file: i,
|
|
134
|
+
message: `Node "${n}" condition "timeIs" must have at least one of "hour" or "day" argument`
|
|
135
|
+
}), a;
|
|
136
|
+
const o = L[e.type];
|
|
137
|
+
if (!o)
|
|
138
|
+
return a;
|
|
139
|
+
for (const r of o)
|
|
140
|
+
(e[r] === void 0 || e[r] === null || e[r] === "") && a.push({
|
|
141
|
+
file: i,
|
|
142
|
+
message: `Node "${n}" condition "${e.type}" missing required "${r}" argument`
|
|
143
|
+
});
|
|
144
|
+
return a;
|
|
123
145
|
}
|
|
124
|
-
function
|
|
125
|
-
const
|
|
146
|
+
function C(e, n, i) {
|
|
147
|
+
const a = [];
|
|
126
148
|
if (!e.type)
|
|
127
|
-
return
|
|
128
|
-
file:
|
|
129
|
-
message: `Node "${
|
|
130
|
-
}),
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
case "addVariable":
|
|
141
|
-
e.variable || n.push({
|
|
142
|
-
file: a,
|
|
143
|
-
message: `Node "${o}" effect "${e.type}" missing required "variable" argument`
|
|
144
|
-
}), e.value === void 0 && n.push({
|
|
145
|
-
file: a,
|
|
146
|
-
message: `Node "${o}" effect "${e.type}" missing required "value" argument`
|
|
147
|
-
});
|
|
148
|
-
break;
|
|
149
|
-
case "addItem":
|
|
150
|
-
case "removeItem":
|
|
151
|
-
e.item || n.push({
|
|
152
|
-
file: a,
|
|
153
|
-
message: `Node "${o}" effect "${e.type}" missing required "item" argument`
|
|
154
|
-
});
|
|
155
|
-
break;
|
|
156
|
-
case "moveItem":
|
|
157
|
-
e.item || n.push({
|
|
158
|
-
file: a,
|
|
159
|
-
message: `Node "${o}" effect "moveItem" missing required "item" argument`
|
|
160
|
-
}), e.location || n.push({
|
|
161
|
-
file: a,
|
|
162
|
-
message: `Node "${o}" effect "moveItem" missing required "location" argument`
|
|
163
|
-
});
|
|
164
|
-
break;
|
|
165
|
-
case "setQuestStage":
|
|
166
|
-
e.quest || n.push({
|
|
167
|
-
file: a,
|
|
168
|
-
message: `Node "${o}" effect "setQuestStage" missing required "quest" argument`
|
|
169
|
-
}), e.stage || n.push({
|
|
170
|
-
file: a,
|
|
171
|
-
message: `Node "${o}" effect "setQuestStage" missing required "stage" argument`
|
|
172
|
-
});
|
|
173
|
-
break;
|
|
174
|
-
case "addJournalEntry":
|
|
175
|
-
e.entry || n.push({
|
|
176
|
-
file: a,
|
|
177
|
-
message: `Node "${o}" effect "addJournalEntry" missing required "entry" argument`
|
|
178
|
-
});
|
|
179
|
-
break;
|
|
180
|
-
case "setCharacterLocation":
|
|
181
|
-
e.character || n.push({
|
|
182
|
-
file: a,
|
|
183
|
-
message: `Node "${o}" effect "setCharacterLocation" missing required "character" argument`
|
|
184
|
-
}), e.location || n.push({
|
|
185
|
-
file: a,
|
|
186
|
-
message: `Node "${o}" effect "setCharacterLocation" missing required "location" argument`
|
|
187
|
-
});
|
|
188
|
-
break;
|
|
189
|
-
case "addToParty":
|
|
190
|
-
case "removeFromParty":
|
|
191
|
-
e.character || n.push({
|
|
192
|
-
file: a,
|
|
193
|
-
message: `Node "${o}" effect "${e.type}" missing required "character" argument`
|
|
194
|
-
});
|
|
195
|
-
break;
|
|
196
|
-
case "setRelationship":
|
|
197
|
-
case "addRelationship":
|
|
198
|
-
e.character || n.push({
|
|
199
|
-
file: a,
|
|
200
|
-
message: `Node "${o}" effect "${e.type}" missing required "character" argument`
|
|
201
|
-
}), e.value === void 0 && n.push({
|
|
202
|
-
file: a,
|
|
203
|
-
message: `Node "${o}" effect "${e.type}" missing required "value" argument`
|
|
204
|
-
});
|
|
205
|
-
break;
|
|
206
|
-
case "setCharacterStat":
|
|
207
|
-
case "addCharacterStat":
|
|
208
|
-
e.character || n.push({
|
|
209
|
-
file: a,
|
|
210
|
-
message: `Node "${o}" effect "${e.type}" missing required "character" argument`
|
|
211
|
-
}), e.stat || n.push({
|
|
212
|
-
file: a,
|
|
213
|
-
message: `Node "${o}" effect "${e.type}" missing required "stat" argument`
|
|
214
|
-
}), e.value === void 0 && n.push({
|
|
215
|
-
file: a,
|
|
216
|
-
message: `Node "${o}" effect "${e.type}" missing required "value" argument`
|
|
217
|
-
});
|
|
218
|
-
break;
|
|
219
|
-
case "setMapEnabled":
|
|
220
|
-
e.enabled === void 0 && n.push({
|
|
221
|
-
file: a,
|
|
222
|
-
message: `Node "${o}" effect "setMapEnabled" missing required "enabled" argument`
|
|
223
|
-
});
|
|
224
|
-
break;
|
|
225
|
-
case "advanceTime":
|
|
226
|
-
e.hours === void 0 && n.push({
|
|
227
|
-
file: a,
|
|
228
|
-
message: `Node "${o}" effect "advanceTime" missing required "hours" argument`
|
|
229
|
-
});
|
|
230
|
-
break;
|
|
231
|
-
case "goToLocation":
|
|
232
|
-
e.location || n.push({
|
|
233
|
-
file: a,
|
|
234
|
-
message: `Node "${o}" effect "goToLocation" missing required "location" argument`
|
|
235
|
-
});
|
|
236
|
-
break;
|
|
237
|
-
case "startDialogue":
|
|
238
|
-
e.dialogue || n.push({
|
|
239
|
-
file: a,
|
|
240
|
-
message: `Node "${o}" effect "startDialogue" missing required "dialogue" argument`
|
|
241
|
-
});
|
|
242
|
-
break;
|
|
243
|
-
case "playMusic":
|
|
244
|
-
case "playSound":
|
|
245
|
-
e.file || n.push({
|
|
246
|
-
file: a,
|
|
247
|
-
message: `Node "${o}" effect "${e.type}" missing required "file" argument`
|
|
248
|
-
});
|
|
249
|
-
break;
|
|
250
|
-
case "playVideo":
|
|
251
|
-
e.file || n.push({
|
|
252
|
-
file: a,
|
|
253
|
-
message: `Node "${o}" effect "playVideo" missing required "file" argument`
|
|
254
|
-
});
|
|
255
|
-
break;
|
|
256
|
-
case "notify":
|
|
257
|
-
e.message || n.push({
|
|
258
|
-
file: a,
|
|
259
|
-
message: `Node "${o}" effect "notify" missing required "message" argument`
|
|
260
|
-
});
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
return n;
|
|
149
|
+
return a.push({
|
|
150
|
+
file: i,
|
|
151
|
+
message: `Node "${n}" has effect with missing type`
|
|
152
|
+
}), a;
|
|
153
|
+
const o = U[e.type];
|
|
154
|
+
if (!o)
|
|
155
|
+
return a;
|
|
156
|
+
for (const r of o)
|
|
157
|
+
(e[r] === void 0 || e[r] === null || e[r] === "") && a.push({
|
|
158
|
+
file: i,
|
|
159
|
+
message: `Node "${n}" effect "${e.type}" missing required "${r}" argument`
|
|
160
|
+
});
|
|
161
|
+
return a;
|
|
264
162
|
}
|
|
265
|
-
function
|
|
266
|
-
const
|
|
163
|
+
function J(e, n) {
|
|
164
|
+
const i = [], a = /* @__PURE__ */ new Set();
|
|
267
165
|
for (const t of Object.values(e.locales))
|
|
268
166
|
for (const l of Object.keys(t))
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
if (!
|
|
273
|
-
const u =
|
|
274
|
-
|
|
167
|
+
a.add(l);
|
|
168
|
+
const o = (t) => t.startsWith("@"), r = (t, l, d) => {
|
|
169
|
+
const h = t.slice(1);
|
|
170
|
+
if (!a.has(h)) {
|
|
171
|
+
const u = n.get(l) || `${d}:${l}`;
|
|
172
|
+
i.push({
|
|
275
173
|
file: u,
|
|
276
174
|
message: `Localization key "${t}" not found in any locale file`,
|
|
277
|
-
suggestion: `Add "${
|
|
175
|
+
suggestion: `Add "${h}: ..." to your locale files`
|
|
278
176
|
});
|
|
279
177
|
}
|
|
280
178
|
};
|
|
281
179
|
for (const t of Object.values(e.locations))
|
|
282
|
-
|
|
180
|
+
o(t.name) && r(t.name, t.id, "location"), o(t.description) && r(t.description, t.id, "location");
|
|
283
181
|
for (const t of Object.values(e.characters))
|
|
284
|
-
|
|
182
|
+
o(t.name) && r(t.name, t.id, "character"), o(t.biography) && r(t.biography, t.id, "character");
|
|
285
183
|
for (const t of Object.values(e.items))
|
|
286
|
-
|
|
184
|
+
o(t.name) && r(t.name, t.id, "item"), o(t.description) && r(t.description, t.id, "item");
|
|
287
185
|
for (const t of Object.values(e.quests)) {
|
|
288
|
-
|
|
186
|
+
o(t.name) && r(t.name, t.id, "quest"), o(t.description) && r(t.description, t.id, "quest");
|
|
289
187
|
for (const l of t.stages)
|
|
290
|
-
|
|
188
|
+
o(l.description) && r(l.description, t.id, "quest");
|
|
291
189
|
}
|
|
292
190
|
for (const t of Object.values(e.journalEntries))
|
|
293
|
-
|
|
191
|
+
o(t.title) && r(t.title, t.id, "journal"), o(t.text) && r(t.text, t.id, "journal");
|
|
294
192
|
for (const t of Object.values(e.dialogues))
|
|
295
193
|
for (const l of t.nodes) {
|
|
296
|
-
|
|
194
|
+
o(l.text) && r(l.text, t.id, "dialogue");
|
|
297
195
|
for (const d of l.choices)
|
|
298
|
-
|
|
196
|
+
o(d.text) && r(d.text, t.id, "dialogue");
|
|
299
197
|
}
|
|
300
|
-
|
|
198
|
+
for (const t of Object.values(e.interludes))
|
|
199
|
+
o(t.text) && r(t.text, t.id, "interlude");
|
|
200
|
+
return i;
|
|
301
201
|
}
|
|
302
|
-
function
|
|
202
|
+
function T(e) {
|
|
303
203
|
if (e.length === 0) {
|
|
304
|
-
console.log(
|
|
204
|
+
console.log(c.green("✓ No validation errors"));
|
|
305
205
|
return;
|
|
306
206
|
}
|
|
307
|
-
console.log(
|
|
207
|
+
console.log(c.red(`
|
|
308
208
|
✗ Found ${e.length} validation error${e.length === 1 ? "" : "s"}:
|
|
309
209
|
`));
|
|
310
|
-
for (const
|
|
311
|
-
console.log(
|
|
210
|
+
for (const n of e)
|
|
211
|
+
console.log(c.bold(n.file) + (n.line ? `:${n.line}` : "")), console.log(" " + c.red(n.message)), n.suggestion && console.log(" " + c.dim(n.suggestion)), console.log();
|
|
312
212
|
}
|
|
313
|
-
const
|
|
314
|
-
async function
|
|
315
|
-
const e = process.cwd(),
|
|
316
|
-
console.log(""), console.log(
|
|
317
|
-
const
|
|
213
|
+
const N = "🐾", W = "✨", Q = "✏️", V = "➕";
|
|
214
|
+
async function Y() {
|
|
215
|
+
const e = process.cwd(), n = s(e, "content");
|
|
216
|
+
console.log(""), console.log(c.bold.magenta(` ${N} Doodle Engine Dev Server ${N}`)), console.log("");
|
|
217
|
+
const i = {
|
|
318
218
|
name: "doodle-content-loader",
|
|
319
|
-
configureServer(
|
|
320
|
-
|
|
219
|
+
configureServer(o) {
|
|
220
|
+
o.middlewares.use("/api/content", async (t, l) => {
|
|
321
221
|
try {
|
|
322
|
-
const d = await
|
|
222
|
+
const d = await P(n);
|
|
323
223
|
l.setHeader("Content-Type", "application/json"), l.end(JSON.stringify(d));
|
|
324
224
|
} catch (d) {
|
|
325
|
-
console.error(
|
|
225
|
+
console.error(c.red(" Error loading content:"), d), l.statusCode = 500, l.end(JSON.stringify({ error: "Failed to load content" }));
|
|
326
226
|
}
|
|
327
227
|
});
|
|
328
|
-
const
|
|
228
|
+
const r = H(n, {
|
|
329
229
|
ignored: /(^|[\/\\])\../,
|
|
330
230
|
persistent: !0
|
|
331
231
|
});
|
|
332
|
-
|
|
333
|
-
console.log(
|
|
232
|
+
r.on("change", async (t) => {
|
|
233
|
+
console.log(c.yellow(` ${Q} Content changed: ${t}`)), await S(n), o.ws.send({
|
|
334
234
|
type: "full-reload",
|
|
335
235
|
path: "*"
|
|
336
236
|
});
|
|
337
|
-
}),
|
|
338
|
-
console.log(
|
|
237
|
+
}), r.on("add", async (t) => {
|
|
238
|
+
console.log(c.green(` ${V} Content added: ${t}`)), await S(n), o.ws.send({
|
|
339
239
|
type: "full-reload",
|
|
340
240
|
path: "*"
|
|
341
241
|
});
|
|
342
242
|
});
|
|
343
243
|
}
|
|
344
|
-
},
|
|
244
|
+
}, a = await j({
|
|
345
245
|
root: e,
|
|
346
|
-
plugins: [
|
|
246
|
+
plugins: [A(), i],
|
|
347
247
|
server: {
|
|
348
248
|
port: 3e3,
|
|
349
249
|
open: !0
|
|
350
250
|
}
|
|
351
251
|
});
|
|
352
|
-
await
|
|
252
|
+
await a.listen(), a.printUrls(), console.log(""), console.log(c.dim(` ${W} Watching content files for changes...`)), console.log("");
|
|
353
253
|
}
|
|
354
|
-
async function
|
|
355
|
-
const
|
|
254
|
+
async function P(e) {
|
|
255
|
+
const n = {
|
|
356
256
|
locations: {},
|
|
357
257
|
characters: {},
|
|
358
258
|
items: {},
|
|
@@ -360,52 +260,54 @@ async function Q(e) {
|
|
|
360
260
|
dialogues: {},
|
|
361
261
|
quests: {},
|
|
362
262
|
journalEntries: {},
|
|
263
|
+
interludes: {},
|
|
363
264
|
locales: {}
|
|
364
265
|
};
|
|
365
|
-
let
|
|
366
|
-
const
|
|
266
|
+
let i = null;
|
|
267
|
+
const a = [
|
|
367
268
|
{ dir: "locations", key: "locations" },
|
|
368
269
|
{ dir: "characters", key: "characters" },
|
|
369
270
|
{ dir: "items", key: "items" },
|
|
370
271
|
{ dir: "maps", key: "maps" },
|
|
371
272
|
{ dir: "quests", key: "quests" },
|
|
372
|
-
{ dir: "journal", key: "journalEntries" }
|
|
273
|
+
{ dir: "journal", key: "journalEntries" },
|
|
274
|
+
{ dir: "interludes", key: "interludes" }
|
|
373
275
|
];
|
|
374
|
-
for (const { dir:
|
|
375
|
-
const t = s(e,
|
|
276
|
+
for (const { dir: o, key: r } of a) {
|
|
277
|
+
const t = s(e, o);
|
|
376
278
|
try {
|
|
377
279
|
const l = await y(t);
|
|
378
280
|
for (const d of l)
|
|
379
|
-
if (
|
|
380
|
-
const
|
|
381
|
-
g && g.id && (
|
|
281
|
+
if (f(d) === ".yaml" || f(d) === ".yml") {
|
|
282
|
+
const h = s(t, d), u = await p(h, "utf-8"), g = b(u);
|
|
283
|
+
g && g.id && (n[r][g.id] = g);
|
|
382
284
|
}
|
|
383
285
|
} catch {
|
|
384
286
|
}
|
|
385
287
|
}
|
|
386
288
|
try {
|
|
387
|
-
const
|
|
388
|
-
for (const t of
|
|
389
|
-
if (
|
|
390
|
-
const l = s(
|
|
391
|
-
|
|
289
|
+
const o = s(e, "locales"), r = await y(o);
|
|
290
|
+
for (const t of r)
|
|
291
|
+
if (f(t) === ".yaml" || f(t) === ".yml") {
|
|
292
|
+
const l = s(o, t), d = await p(l, "utf-8"), h = b(d), u = t.replace(/\.(yaml|yml)$/, "");
|
|
293
|
+
n.locales[u] = h ?? {};
|
|
392
294
|
}
|
|
393
295
|
} catch {
|
|
394
296
|
}
|
|
395
297
|
try {
|
|
396
|
-
const
|
|
397
|
-
for (const t of
|
|
398
|
-
if (
|
|
399
|
-
const l = s(
|
|
400
|
-
|
|
298
|
+
const o = s(e, "dialogues"), r = await y(o);
|
|
299
|
+
for (const t of r)
|
|
300
|
+
if (f(t) === ".dlg") {
|
|
301
|
+
const l = s(o, t), d = await p(l, "utf-8"), h = t.replace(".dlg", ""), u = v(d, h);
|
|
302
|
+
n.dialogues[u.id] = u;
|
|
401
303
|
}
|
|
402
304
|
} catch {
|
|
403
305
|
}
|
|
404
306
|
try {
|
|
405
|
-
const
|
|
406
|
-
|
|
307
|
+
const o = s(e, "game.yaml"), r = await p(o, "utf-8");
|
|
308
|
+
i = b(r);
|
|
407
309
|
} catch {
|
|
408
|
-
console.warn(
|
|
310
|
+
console.warn(c.yellow(" No game.yaml found, using defaults")), i = {
|
|
409
311
|
startLocation: "tavern",
|
|
410
312
|
startTime: { day: 1, hour: 8 },
|
|
411
313
|
startFlags: {},
|
|
@@ -413,18 +315,18 @@ async function Q(e) {
|
|
|
413
315
|
startInventory: []
|
|
414
316
|
};
|
|
415
317
|
}
|
|
416
|
-
return { registry:
|
|
318
|
+
return { registry: n, config: i };
|
|
417
319
|
}
|
|
418
|
-
async function
|
|
320
|
+
async function S(e) {
|
|
419
321
|
try {
|
|
420
|
-
const { registry:
|
|
421
|
-
|
|
422
|
-
} catch (
|
|
423
|
-
console.error(
|
|
322
|
+
const { registry: n, fileMap: i } = await K(e), a = I(n, i);
|
|
323
|
+
a.length > 0 && (console.log(""), T(a), console.log(""));
|
|
324
|
+
} catch (n) {
|
|
325
|
+
console.error(c.red(" Error running validation:"), n);
|
|
424
326
|
}
|
|
425
327
|
}
|
|
426
|
-
async function
|
|
427
|
-
const
|
|
328
|
+
async function K(e) {
|
|
329
|
+
const n = {
|
|
428
330
|
locations: {},
|
|
429
331
|
characters: {},
|
|
430
332
|
items: {},
|
|
@@ -432,72 +334,77 @@ async function V(e) {
|
|
|
432
334
|
dialogues: {},
|
|
433
335
|
quests: {},
|
|
434
336
|
journalEntries: {},
|
|
337
|
+
interludes: {},
|
|
435
338
|
locales: {}
|
|
436
|
-
},
|
|
339
|
+
}, i = /* @__PURE__ */ new Map(), a = [
|
|
437
340
|
{ dir: "locations", key: "locations" },
|
|
438
341
|
{ dir: "characters", key: "characters" },
|
|
439
342
|
{ dir: "items", key: "items" },
|
|
440
343
|
{ dir: "maps", key: "maps" },
|
|
441
344
|
{ dir: "quests", key: "quests" },
|
|
442
|
-
{ dir: "journal", key: "journalEntries" }
|
|
345
|
+
{ dir: "journal", key: "journalEntries" },
|
|
346
|
+
{ dir: "interludes", key: "interludes" }
|
|
443
347
|
];
|
|
444
|
-
for (const { dir:
|
|
445
|
-
const t = s(e,
|
|
348
|
+
for (const { dir: o, key: r } of a) {
|
|
349
|
+
const t = s(e, o);
|
|
446
350
|
try {
|
|
447
351
|
const l = await y(t);
|
|
448
352
|
for (const d of l)
|
|
449
|
-
if (
|
|
450
|
-
const
|
|
451
|
-
g && g.id && (
|
|
353
|
+
if (f(d) === ".yaml" || f(d) === ".yml") {
|
|
354
|
+
const h = s(t, d), u = await p(h, "utf-8"), g = b(u);
|
|
355
|
+
g && g.id && (n[r][g.id] = g, i.set(g.id, w(process.cwd(), h)));
|
|
452
356
|
}
|
|
453
357
|
} catch {
|
|
454
358
|
}
|
|
455
359
|
}
|
|
456
360
|
try {
|
|
457
|
-
const
|
|
458
|
-
for (const t of
|
|
459
|
-
if (
|
|
460
|
-
const l = s(
|
|
461
|
-
|
|
361
|
+
const o = s(e, "locales"), r = await y(o);
|
|
362
|
+
for (const t of r)
|
|
363
|
+
if (f(t) === ".yaml" || f(t) === ".yml") {
|
|
364
|
+
const l = s(o, t), d = await p(l, "utf-8"), h = b(d), u = t.replace(/\.(yaml|yml)$/, "");
|
|
365
|
+
n.locales[u] = h ?? {};
|
|
462
366
|
}
|
|
463
367
|
} catch {
|
|
464
368
|
}
|
|
465
369
|
try {
|
|
466
|
-
const
|
|
467
|
-
for (const t of
|
|
468
|
-
if (
|
|
469
|
-
const l = s(
|
|
470
|
-
|
|
370
|
+
const o = s(e, "dialogues"), r = await y(o);
|
|
371
|
+
for (const t of r)
|
|
372
|
+
if (f(t) === ".dlg") {
|
|
373
|
+
const l = s(o, t), d = await p(l, "utf-8"), h = t.replace(".dlg", ""), u = v(d, h);
|
|
374
|
+
n.dialogues[u.id] = u, i.set(u.id, w(process.cwd(), l));
|
|
471
375
|
}
|
|
472
376
|
} catch {
|
|
473
377
|
}
|
|
474
|
-
return { registry:
|
|
378
|
+
return { registry: n, fileMap: i };
|
|
475
379
|
}
|
|
476
|
-
async function
|
|
477
|
-
const e = process.cwd(),
|
|
478
|
-
console.log(""), console.log(
|
|
380
|
+
async function z() {
|
|
381
|
+
const e = process.cwd(), n = s(e, "content");
|
|
382
|
+
console.log(""), console.log(c.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(c.dim("Validating content..."));
|
|
383
|
+
let i;
|
|
479
384
|
try {
|
|
480
|
-
const { registry: a, fileMap:
|
|
481
|
-
|
|
385
|
+
const { registry: a, fileMap: o, config: r } = await X(n), t = I(a, o);
|
|
386
|
+
T(t), t.length > 0 && (console.log(c.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), i = { registry: a, config: r };
|
|
482
387
|
} catch (a) {
|
|
483
|
-
console.error(
|
|
388
|
+
console.error(c.red("Error loading content:"), a), process.exit(1);
|
|
484
389
|
}
|
|
485
390
|
console.log("");
|
|
486
391
|
try {
|
|
487
|
-
await
|
|
392
|
+
await $({
|
|
488
393
|
root: e,
|
|
489
|
-
plugins: [
|
|
394
|
+
plugins: [A()],
|
|
490
395
|
build: {
|
|
491
396
|
outDir: "dist",
|
|
492
397
|
emptyOutDir: !0
|
|
493
398
|
}
|
|
494
|
-
})
|
|
399
|
+
});
|
|
400
|
+
const a = s(e, "dist", "api");
|
|
401
|
+
await x(a, { recursive: !0 }), await m(s(a, "content"), JSON.stringify(i)), console.log(""), console.log(c.green("✅ Build complete! Output in dist/")), console.log(""), console.log("To preview the build:"), console.log(c.dim(" yarn preview")), console.log("");
|
|
495
402
|
} catch (a) {
|
|
496
|
-
console.error(
|
|
403
|
+
console.error(c.red("Build failed:"), a), process.exit(1);
|
|
497
404
|
}
|
|
498
405
|
}
|
|
499
|
-
async function
|
|
500
|
-
const
|
|
406
|
+
async function X(e) {
|
|
407
|
+
const n = {
|
|
501
408
|
locations: {},
|
|
502
409
|
characters: {},
|
|
503
410
|
items: {},
|
|
@@ -505,59 +412,68 @@ async function P(e) {
|
|
|
505
412
|
dialogues: {},
|
|
506
413
|
quests: {},
|
|
507
414
|
journalEntries: {},
|
|
415
|
+
interludes: {},
|
|
508
416
|
locales: {}
|
|
509
|
-
},
|
|
417
|
+
}, i = /* @__PURE__ */ new Map(), a = [
|
|
510
418
|
{ dir: "locations", key: "locations" },
|
|
511
419
|
{ dir: "characters", key: "characters" },
|
|
512
420
|
{ dir: "items", key: "items" },
|
|
513
421
|
{ dir: "maps", key: "maps" },
|
|
514
422
|
{ dir: "quests", key: "quests" },
|
|
515
|
-
{ dir: "journal", key: "journalEntries" }
|
|
423
|
+
{ dir: "journal", key: "journalEntries" },
|
|
424
|
+
{ dir: "interludes", key: "interludes" }
|
|
516
425
|
];
|
|
517
|
-
for (const { dir: r, key:
|
|
518
|
-
const
|
|
426
|
+
for (const { dir: r, key: t } of a) {
|
|
427
|
+
const l = s(e, r);
|
|
519
428
|
try {
|
|
520
|
-
const
|
|
521
|
-
for (const
|
|
522
|
-
if (
|
|
523
|
-
const
|
|
524
|
-
|
|
429
|
+
const d = await y(l);
|
|
430
|
+
for (const h of d)
|
|
431
|
+
if (f(h) === ".yaml" || f(h) === ".yml") {
|
|
432
|
+
const u = s(l, h), g = await p(u, "utf-8"), E = b(g);
|
|
433
|
+
E && E.id && (n[t][E.id] = E, i.set(E.id, w(process.cwd(), u)));
|
|
525
434
|
}
|
|
526
435
|
} catch {
|
|
527
436
|
}
|
|
528
437
|
}
|
|
529
438
|
try {
|
|
530
|
-
const r = s(e, "locales"),
|
|
531
|
-
for (const
|
|
532
|
-
if (
|
|
533
|
-
const
|
|
534
|
-
|
|
439
|
+
const r = s(e, "locales"), t = await y(r);
|
|
440
|
+
for (const l of t)
|
|
441
|
+
if (f(l) === ".yaml" || f(l) === ".yml") {
|
|
442
|
+
const d = s(r, l), h = await p(d, "utf-8"), u = b(h), g = l.replace(/\.(yaml|yml)$/, "");
|
|
443
|
+
n.locales[g] = u ?? {};
|
|
535
444
|
}
|
|
536
445
|
} catch {
|
|
537
446
|
}
|
|
538
447
|
try {
|
|
539
|
-
const r = s(e, "dialogues"),
|
|
540
|
-
for (const
|
|
541
|
-
if (
|
|
542
|
-
const
|
|
543
|
-
|
|
448
|
+
const r = s(e, "dialogues"), t = await y(r);
|
|
449
|
+
for (const l of t)
|
|
450
|
+
if (f(l) === ".dlg") {
|
|
451
|
+
const d = s(r, l), h = await p(d, "utf-8"), u = l.replace(".dlg", ""), g = v(h, u);
|
|
452
|
+
n.dialogues[g.id] = g, i.set(g.id, w(process.cwd(), d));
|
|
544
453
|
}
|
|
545
454
|
} catch {
|
|
546
455
|
}
|
|
547
|
-
|
|
456
|
+
let o = null;
|
|
457
|
+
try {
|
|
458
|
+
const r = s(e, "game.yaml"), t = await p(r, "utf-8");
|
|
459
|
+
o = b(t);
|
|
460
|
+
} catch {
|
|
461
|
+
o = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
|
|
462
|
+
}
|
|
463
|
+
return { registry: n, fileMap: i, config: o };
|
|
548
464
|
}
|
|
549
|
-
async function
|
|
550
|
-
const e = process.cwd(),
|
|
551
|
-
console.log(""), console.log(
|
|
465
|
+
async function Z() {
|
|
466
|
+
const e = process.cwd(), n = s(e, "content");
|
|
467
|
+
console.log(""), console.log(c.bold.magenta("🐾 Validating Doodle Engine content...")), console.log("");
|
|
552
468
|
try {
|
|
553
|
-
const { registry:
|
|
554
|
-
|
|
555
|
-
} catch (
|
|
556
|
-
console.error(
|
|
469
|
+
const { registry: i, fileMap: a } = await ee(n), o = I(i, a);
|
|
470
|
+
T(o), o.length > 0 && process.exit(1);
|
|
471
|
+
} catch (i) {
|
|
472
|
+
console.error(c.red("Error loading content:"), i), process.exit(1);
|
|
557
473
|
}
|
|
558
474
|
}
|
|
559
|
-
async function
|
|
560
|
-
const
|
|
475
|
+
async function ee(e) {
|
|
476
|
+
const n = {
|
|
561
477
|
locations: {},
|
|
562
478
|
characters: {},
|
|
563
479
|
items: {},
|
|
@@ -566,7 +482,7 @@ async function z(e) {
|
|
|
566
482
|
quests: {},
|
|
567
483
|
journalEntries: {},
|
|
568
484
|
locales: {}
|
|
569
|
-
},
|
|
485
|
+
}, i = /* @__PURE__ */ new Map(), a = [
|
|
570
486
|
{ dir: "locations", key: "locations" },
|
|
571
487
|
{ dir: "characters", key: "characters" },
|
|
572
488
|
{ dir: "items", key: "items" },
|
|
@@ -574,59 +490,60 @@ async function z(e) {
|
|
|
574
490
|
{ dir: "quests", key: "quests" },
|
|
575
491
|
{ dir: "journal", key: "journalEntries" }
|
|
576
492
|
];
|
|
577
|
-
for (const { dir:
|
|
578
|
-
const t = s(e,
|
|
493
|
+
for (const { dir: o, key: r } of a) {
|
|
494
|
+
const t = s(e, o);
|
|
579
495
|
try {
|
|
580
496
|
const l = await y(t);
|
|
581
497
|
for (const d of l)
|
|
582
|
-
if (
|
|
583
|
-
const
|
|
584
|
-
g && g.id && (
|
|
498
|
+
if (f(d) === ".yaml" || f(d) === ".yml") {
|
|
499
|
+
const h = s(t, d), u = await p(h, "utf-8"), g = b(u);
|
|
500
|
+
g && g.id && (n[r][g.id] = g, i.set(g.id, w(process.cwd(), h)));
|
|
585
501
|
}
|
|
586
502
|
} catch {
|
|
587
503
|
}
|
|
588
504
|
}
|
|
589
505
|
try {
|
|
590
|
-
const
|
|
591
|
-
for (const t of
|
|
592
|
-
if (
|
|
593
|
-
const l = s(
|
|
594
|
-
|
|
506
|
+
const o = s(e, "locales"), r = await y(o);
|
|
507
|
+
for (const t of r)
|
|
508
|
+
if (f(t) === ".yaml" || f(t) === ".yml") {
|
|
509
|
+
const l = s(o, t), d = await p(l, "utf-8"), h = b(d), u = t.replace(/\.(yaml|yml)$/, "");
|
|
510
|
+
n.locales[u] = h ?? {};
|
|
595
511
|
}
|
|
596
512
|
} catch {
|
|
597
513
|
}
|
|
598
514
|
try {
|
|
599
|
-
const
|
|
600
|
-
for (const t of
|
|
601
|
-
if (
|
|
602
|
-
const l = s(
|
|
603
|
-
|
|
515
|
+
const o = s(e, "dialogues"), r = await y(o);
|
|
516
|
+
for (const t of r)
|
|
517
|
+
if (f(t) === ".dlg") {
|
|
518
|
+
const l = s(o, t), d = await p(l, "utf-8"), h = t.replace(".dlg", ""), u = v(d, h);
|
|
519
|
+
n.dialogues[u.id] = u, i.set(u.id, w(process.cwd(), l));
|
|
604
520
|
}
|
|
605
521
|
} catch {
|
|
606
522
|
}
|
|
607
|
-
return { registry:
|
|
523
|
+
return { registry: n, fileMap: i };
|
|
608
524
|
}
|
|
609
|
-
const
|
|
610
|
-
async function
|
|
611
|
-
const
|
|
612
|
-
console.log(""), console.log(
|
|
613
|
-
const { useDefaultRenderer:
|
|
525
|
+
const D = "🐾", te = "🐕", R = "🦴", 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({
|
|
614
530
|
type: "confirm",
|
|
615
531
|
name: "useDefaultRenderer",
|
|
616
532
|
message: "Use default renderer?",
|
|
617
533
|
initial: !0
|
|
618
534
|
});
|
|
619
|
-
|
|
620
|
-
${
|
|
535
|
+
i === void 0 && (console.log(c.yellow(`
|
|
536
|
+
${R} 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("");
|
|
621
537
|
}
|
|
622
|
-
async function
|
|
623
|
-
const
|
|
538
|
+
async function re(e, n, i) {
|
|
539
|
+
const a = [
|
|
624
540
|
"content/locations",
|
|
625
541
|
"content/characters",
|
|
626
542
|
"content/items",
|
|
627
543
|
"content/dialogues",
|
|
628
544
|
"content/quests",
|
|
629
545
|
"content/journal",
|
|
546
|
+
"content/interludes",
|
|
630
547
|
"content/locales",
|
|
631
548
|
"content/maps",
|
|
632
549
|
"assets/images/banners",
|
|
@@ -638,12 +555,12 @@ async function oe(e, o, a) {
|
|
|
638
555
|
"assets/audio/voice",
|
|
639
556
|
"src"
|
|
640
557
|
];
|
|
641
|
-
console.log(` ${
|
|
642
|
-
for (const u of
|
|
643
|
-
await
|
|
644
|
-
console.log(
|
|
645
|
-
const
|
|
646
|
-
name:
|
|
558
|
+
console.log(` ${q} ${c.bold("Creating directories...")}`);
|
|
559
|
+
for (const u of a)
|
|
560
|
+
await x(s(e, u), { recursive: !0 });
|
|
561
|
+
console.log(c.green(` ${_} Directories created`)), console.log("");
|
|
562
|
+
const o = {
|
|
563
|
+
name: n,
|
|
647
564
|
version: "0.1.0",
|
|
648
565
|
type: "module",
|
|
649
566
|
scripts: {
|
|
@@ -666,11 +583,11 @@ async function oe(e, o, a) {
|
|
|
666
583
|
vite: "^6.0.0"
|
|
667
584
|
}
|
|
668
585
|
};
|
|
669
|
-
console.log(` ${
|
|
586
|
+
console.log(` ${oe} ${c.bold("Writing project files...")}`), await m(
|
|
670
587
|
s(e, "package.json"),
|
|
671
|
-
JSON.stringify(
|
|
588
|
+
JSON.stringify(o, null, 2)
|
|
672
589
|
);
|
|
673
|
-
const
|
|
590
|
+
const r = {
|
|
674
591
|
compilerOptions: {
|
|
675
592
|
target: "ES2024",
|
|
676
593
|
lib: ["ES2024", "DOM", "DOM.Iterable"],
|
|
@@ -687,10 +604,10 @@ async function oe(e, o, a) {
|
|
|
687
604
|
},
|
|
688
605
|
include: ["src"]
|
|
689
606
|
};
|
|
690
|
-
await
|
|
607
|
+
await m(
|
|
691
608
|
s(e, "tsconfig.json"),
|
|
692
|
-
JSON.stringify(
|
|
693
|
-
), await
|
|
609
|
+
JSON.stringify(r, null, 2)
|
|
610
|
+
), await m(s(e, "index.html"), `<!doctype html>
|
|
694
611
|
<html lang="en">
|
|
695
612
|
<head>
|
|
696
613
|
<meta charset="UTF-8" />
|
|
@@ -702,7 +619,7 @@ async function oe(e, o, a) {
|
|
|
702
619
|
<script type="module" src="/src/main.tsx"><\/script>
|
|
703
620
|
</body>
|
|
704
621
|
</html>
|
|
705
|
-
`), await
|
|
622
|
+
`), await m(s(e, "src/main.tsx"), `import { StrictMode } from 'react'
|
|
706
623
|
import { createRoot } from 'react-dom/client'
|
|
707
624
|
import { App } from './App'
|
|
708
625
|
import './index.css'
|
|
@@ -714,9 +631,9 @@ createRoot(document.getElementById('root')!).render(
|
|
|
714
631
|
)
|
|
715
632
|
`);
|
|
716
633
|
let d;
|
|
717
|
-
|
|
634
|
+
i ? d = `import { useEffect, useState } from 'react'
|
|
718
635
|
import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
|
|
719
|
-
import { GameShell } from '@doodle-engine/react'
|
|
636
|
+
import { GameShell, LoadingScreen } from '@doodle-engine/react'
|
|
720
637
|
|
|
721
638
|
export function App() {
|
|
722
639
|
const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
|
|
@@ -728,7 +645,7 @@ export function App() {
|
|
|
728
645
|
}, [])
|
|
729
646
|
|
|
730
647
|
if (!content) {
|
|
731
|
-
return <
|
|
648
|
+
return <LoadingScreen />
|
|
732
649
|
}
|
|
733
650
|
|
|
734
651
|
return (
|
|
@@ -739,13 +656,14 @@ export function App() {
|
|
|
739
656
|
subtitle="A text-based adventure"
|
|
740
657
|
splashDuration={2000}
|
|
741
658
|
availableLocales={[{ code: 'en', label: 'English' }]}
|
|
659
|
+
devTools={import.meta.env.DEV}
|
|
742
660
|
/>
|
|
743
661
|
)
|
|
744
662
|
}
|
|
745
663
|
` : d = `import { useEffect, useState } from 'react'
|
|
746
664
|
import { Engine } from '@doodle-engine/core'
|
|
747
665
|
import type { GameState, Snapshot } from '@doodle-engine/core'
|
|
748
|
-
import { GameProvider, useGame } from '@doodle-engine/react'
|
|
666
|
+
import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
|
|
749
667
|
|
|
750
668
|
export function App() {
|
|
751
669
|
const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
|
|
@@ -761,11 +679,11 @@ export function App() {
|
|
|
761
679
|
}, [])
|
|
762
680
|
|
|
763
681
|
if (!game) {
|
|
764
|
-
return <
|
|
682
|
+
return <LoadingScreen />
|
|
765
683
|
}
|
|
766
684
|
|
|
767
685
|
return (
|
|
768
|
-
<GameProvider engine={game.engine} initialSnapshot={game.snapshot}>
|
|
686
|
+
<GameProvider engine={game.engine} initialSnapshot={game.snapshot} devTools={import.meta.env.DEV}>
|
|
769
687
|
<GameUI />
|
|
770
688
|
</GameProvider>
|
|
771
689
|
)
|
|
@@ -833,7 +751,7 @@ function createEmptyState(): GameState {
|
|
|
833
751
|
currentLocale: 'en',
|
|
834
752
|
}
|
|
835
753
|
}
|
|
836
|
-
`, await
|
|
754
|
+
`, await m(s(e, "src/App.tsx"), d), await m(s(e, "src/index.css"), `body {
|
|
837
755
|
margin: 0;
|
|
838
756
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
839
757
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
@@ -841,7 +759,7 @@ function createEmptyState(): GameState {
|
|
|
841
759
|
-webkit-font-smoothing: antialiased;
|
|
842
760
|
-moz-osx-font-smoothing: grayscale;
|
|
843
761
|
}
|
|
844
|
-
`), console.log(
|
|
762
|
+
`), console.log(c.green(` ${_} Source files created`)), console.log(""), console.log(` ${R} ${c.bold("Writing starter content...")}`), await m(s(e, "content/game.yaml"), `# Game Configuration
|
|
845
763
|
startLocation: tavern
|
|
846
764
|
startTime:
|
|
847
765
|
day: 1
|
|
@@ -852,40 +770,40 @@ startVariables:
|
|
|
852
770
|
reputation: 0
|
|
853
771
|
_drinksBought: 0
|
|
854
772
|
startInventory: []
|
|
855
|
-
`), await
|
|
773
|
+
`), await m(s(e, "content/locations/tavern.yaml"), `id: tavern
|
|
856
774
|
name: "@location.tavern.name"
|
|
857
775
|
description: "@location.tavern.description"
|
|
858
776
|
banner: ""
|
|
859
777
|
music: ""
|
|
860
778
|
ambient: ""
|
|
861
|
-
`), await
|
|
779
|
+
`), await m(s(e, "content/locations/market.yaml"), `id: market
|
|
862
780
|
name: "@location.market.name"
|
|
863
781
|
description: "@location.market.description"
|
|
864
782
|
banner: ""
|
|
865
783
|
music: ""
|
|
866
784
|
ambient: ""
|
|
867
|
-
`), await
|
|
785
|
+
`), await m(s(e, "content/characters/bartender.yaml"), `id: bartender
|
|
868
786
|
name: "@character.bartender.name"
|
|
869
787
|
biography: "@character.bartender.bio"
|
|
870
788
|
portrait: ""
|
|
871
789
|
location: tavern
|
|
872
790
|
dialogue: bartender_greeting
|
|
873
791
|
stats: {}
|
|
874
|
-
`), await
|
|
792
|
+
`), await m(s(e, "content/characters/merchant.yaml"), `id: merchant
|
|
875
793
|
name: "@character.merchant.name"
|
|
876
794
|
biography: "@character.merchant.bio"
|
|
877
795
|
portrait: ""
|
|
878
796
|
location: market
|
|
879
797
|
dialogue: merchant_intro
|
|
880
798
|
stats: {}
|
|
881
|
-
`), await
|
|
799
|
+
`), await m(s(e, "content/items/old_coin.yaml"), `id: old_coin
|
|
882
800
|
name: "@item.old_coin.name"
|
|
883
801
|
description: "@item.old_coin.description"
|
|
884
802
|
icon: ""
|
|
885
803
|
image: ""
|
|
886
804
|
location: tavern
|
|
887
805
|
stats: {}
|
|
888
|
-
`), await
|
|
806
|
+
`), await m(s(e, "content/maps/town.yaml"), `id: town
|
|
889
807
|
name: "@map.town.name"
|
|
890
808
|
image: ""
|
|
891
809
|
scale: 1
|
|
@@ -896,7 +814,7 @@ locations:
|
|
|
896
814
|
- id: market
|
|
897
815
|
x: 300
|
|
898
816
|
y: 150
|
|
899
|
-
`), await
|
|
817
|
+
`), await m(s(e, "content/quests/odd_jobs.yaml"), `id: odd_jobs
|
|
900
818
|
name: "@quest.odd_jobs.name"
|
|
901
819
|
description: "@quest.odd_jobs.description"
|
|
902
820
|
stages:
|
|
@@ -906,40 +824,90 @@ stages:
|
|
|
906
824
|
description: "@quest.odd_jobs.stage.talked_to_merchant"
|
|
907
825
|
- id: complete
|
|
908
826
|
description: "@quest.odd_jobs.stage.complete"
|
|
909
|
-
`), await
|
|
827
|
+
`), await m(s(e, "content/journal/tavern_discovery.yaml"), `id: tavern_discovery
|
|
910
828
|
title: "@journal.tavern_discovery.title"
|
|
911
829
|
text: "@journal.tavern_discovery.text"
|
|
912
830
|
category: places
|
|
913
|
-
`), await
|
|
831
|
+
`), await m(s(e, "content/journal/odd_jobs_accepted.yaml"), `id: odd_jobs_accepted
|
|
914
832
|
title: "@journal.odd_jobs_accepted.title"
|
|
915
833
|
text: "@journal.odd_jobs_accepted.text"
|
|
916
834
|
category: quests
|
|
917
|
-
`), await
|
|
835
|
+
`), await m(s(e, "content/journal/market_square.yaml"), `id: market_square
|
|
918
836
|
title: "@journal.market_square.title"
|
|
919
837
|
text: "@journal.market_square.text"
|
|
920
838
|
category: places
|
|
921
|
-
`), await
|
|
839
|
+
`), await m(s(e, "content/interludes/chapter_one.yaml"), `id: chapter_one
|
|
840
|
+
# Background image shown fullscreen during the interlude.
|
|
841
|
+
# Put your image in assets/images/ and reference it here.
|
|
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. Use @localization.key for multi-language support,
|
|
851
|
+
# or write plain text directly.
|
|
852
|
+
text: |
|
|
853
|
+
Chapter One: A New Beginning
|
|
854
|
+
|
|
855
|
+
The road behind you stretches long and empty.
|
|
856
|
+
Ahead, the lights of town flicker through the evening mist.
|
|
857
|
+
|
|
858
|
+
You have heard the rumours. Strange things happening.
|
|
859
|
+
People going missing. Shadows that move wrong.
|
|
860
|
+
|
|
861
|
+
Someone has to look into it.
|
|
862
|
+
|
|
863
|
+
It might as well be you.
|
|
864
|
+
|
|
865
|
+
# triggerLocation: tavern
|
|
866
|
+
# triggerConditions:
|
|
867
|
+
# - type: notFlag
|
|
868
|
+
# flag: seenChapterOne
|
|
869
|
+
`), await m(s(e, "content/dialogues/tavern_intro.dlg"), `# This dialogue triggers automatically when the player enters the tavern.
|
|
870
|
+
# TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
|
|
871
|
+
# Use notFlag to make it a one-time intro.
|
|
872
|
+
|
|
873
|
+
TRIGGER tavern
|
|
922
874
|
REQUIRE notFlag seenTavernIntro
|
|
923
875
|
|
|
876
|
+
# Each NODE is a conversation point. The first NODE is always the start.
|
|
924
877
|
NODE start
|
|
878
|
+
# NARRATOR: has no speaker — used for scene-setting text.
|
|
879
|
+
# @narrator.tavern_intro is a localization key defined in content/locales/en.yaml.
|
|
880
|
+
# You can also write text inline: NARRATOR: "The tavern is warm and smells of ale."
|
|
925
881
|
NARRATOR: @narrator.tavern_intro
|
|
882
|
+
|
|
883
|
+
# Effects run immediately when this node is reached, before choices are shown.
|
|
926
884
|
SET flag seenTavernIntro
|
|
927
885
|
|
|
886
|
+
# CHOICE text can use a @key or "inline text".
|
|
887
|
+
# A choice with END dialogue is a terminal choice — no GOTO needed.
|
|
928
888
|
CHOICE @narrator.choice.look_around
|
|
929
889
|
END dialogue
|
|
930
890
|
END
|
|
931
|
-
`), await
|
|
891
|
+
`), await m(s(e, "content/dialogues/market_intro.dlg"), `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
|
|
892
|
+
|
|
893
|
+
TRIGGER market
|
|
932
894
|
REQUIRE notFlag seenMarketIntro
|
|
933
895
|
|
|
934
896
|
NODE start
|
|
935
897
|
NARRATOR: @narrator.market_intro
|
|
936
898
|
SET flag seenMarketIntro
|
|
899
|
+
|
|
900
|
+
# ADD journalEntry unlocks a journal entry for the player.
|
|
937
901
|
ADD journalEntry market_square
|
|
938
902
|
|
|
939
903
|
CHOICE @narrator.choice.look_around
|
|
940
904
|
END dialogue
|
|
941
905
|
END
|
|
942
|
-
`), await
|
|
906
|
+
`), await m(s(e, "content/dialogues/bartender_greeting.dlg"), `# This dialogue is triggered by clicking the bartender character.
|
|
907
|
+
# SPEAKER: lines set who's talking — matched to character ID (case-insensitive).
|
|
908
|
+
# Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
|
|
909
|
+
|
|
910
|
+
NODE start
|
|
943
911
|
BARTENDER: @bartender.greeting
|
|
944
912
|
|
|
945
913
|
# Always available — ask for rumors (demonstrates: flag, relationship, journalEntry)
|
|
@@ -1077,7 +1045,10 @@ NODE work_done
|
|
|
1077
1045
|
NODE farewell
|
|
1078
1046
|
BARTENDER: @bartender.farewell
|
|
1079
1047
|
END dialogue
|
|
1080
|
-
`), await
|
|
1048
|
+
`), await m(s(e, "content/dialogues/merchant_intro.dlg"), `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
|
|
1049
|
+
# The quest choices here demonstrate multi-stage quest gating with questAtStage.
|
|
1050
|
+
|
|
1051
|
+
NODE start
|
|
1081
1052
|
MERCHANT: @merchant.greeting
|
|
1082
1053
|
|
|
1083
1054
|
CHOICE @merchant.choice.browse_wares
|
|
@@ -1177,7 +1148,7 @@ NODE about_market
|
|
|
1177
1148
|
NODE farewell
|
|
1178
1149
|
MERCHANT: @merchant.farewell
|
|
1179
1150
|
END dialogue
|
|
1180
|
-
`), await
|
|
1151
|
+
`), await m(s(e, "content/locales/en.yaml"), `# ===================
|
|
1181
1152
|
# Narrator Intros
|
|
1182
1153
|
# ===================
|
|
1183
1154
|
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."
|
|
@@ -1305,24 +1276,24 @@ notification.quest_updated: "Quest Updated: Odd Jobs"
|
|
|
1305
1276
|
notification.quest_complete: "Quest Complete: Odd Jobs (+50 gold, +10 reputation)"
|
|
1306
1277
|
notification.bought_drink: "Bought a drink (-5 gold)"
|
|
1307
1278
|
notification.bought_map: "Bought a map (-20 gold)"
|
|
1308
|
-
`), console.log(
|
|
1279
|
+
`), console.log(c.green(` ${_} Starter content created`)), console.log(""), console.log(c.dim(" Content includes:")), console.log(c.dim(" 2 locations (tavern, market)")), console.log(c.dim(" 2 characters (bartender, merchant)")), console.log(c.dim(" 1 item (old coin)")), console.log(c.dim(" 1 map (town)")), console.log(c.dim(" 1 quest with 3 stages")), console.log(c.dim(" 3 journal entries")), console.log(c.dim(" 4 dialogues (2 narrator intros, 2 NPC conversations)")), console.log(c.dim(" English locale with all strings")), await m(s(e, ".gitignore"), `node_modules
|
|
1309
1280
|
dist
|
|
1310
1281
|
.DS_Store
|
|
1311
1282
|
*.log
|
|
1312
1283
|
`);
|
|
1313
1284
|
}
|
|
1314
|
-
const k = new
|
|
1315
|
-
k.name("doodle").description(
|
|
1285
|
+
const k = new G();
|
|
1286
|
+
k.name("doodle").description(c.magenta("🐾 Doodle Engine") + c.dim(" — Narrative RPG development tools")).version("0.0.1");
|
|
1316
1287
|
k.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (e) => {
|
|
1317
|
-
await
|
|
1288
|
+
await ne(e);
|
|
1318
1289
|
});
|
|
1319
1290
|
k.command("dev").description("Start development server with hot reload").action(async () => {
|
|
1320
|
-
await
|
|
1291
|
+
await Y();
|
|
1321
1292
|
});
|
|
1322
1293
|
k.command("build").description("Build game for production").action(async () => {
|
|
1323
|
-
await
|
|
1294
|
+
await z();
|
|
1324
1295
|
});
|
|
1325
1296
|
k.command("validate").description("Validate game content").action(async () => {
|
|
1326
|
-
await
|
|
1297
|
+
await Z();
|
|
1327
1298
|
});
|
|
1328
1299
|
k.parse();
|