@doodle-engine/cli 0.0.18 → 0.0.20

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/dist/cli.js CHANGED
@@ -1,84 +1,84 @@
1
1
  #!/usr/bin/env node
2
- import { Command as x } from "commander";
3
- import { crayon as s } from "crayon.js";
4
- import { createServer as q, build as $ } from "vite";
5
- import S from "@vitejs/plugin-react";
6
- import { watch as H } from "chokidar";
7
- import { readdir as p, readFile as f, mkdir as D, writeFile as w } from "fs/promises";
8
- import { join as h, extname as g, relative as y, dirname as M } from "path";
9
- import { parse as b } from "yaml";
10
- import { parseDialogue as k } from "@doodle-engine/core";
11
- import F from "prompts";
12
- function N(n, a) {
13
- const r = [];
14
- for (const o of Object.values(n.dialogues)) {
15
- const t = a.get(o.id) || `dialogue:${o.id}`;
16
- r.push(...B(o, t));
2
+ import { Command as M } from "commander";
3
+ import { crayon as c } from "crayon.js";
4
+ import { createServer as F, build as P } from "vite";
5
+ import G from "@vitejs/plugin-react";
6
+ import { watch as L } from "chokidar";
7
+ import { stat as B, readdir as b, readFile as p, mkdir as T, writeFile as w } from "fs/promises";
8
+ import { join as h, extname as f, relative as E, dirname as U } from "path";
9
+ import { parse as y } from "yaml";
10
+ import { extractAssetPaths as W, getAssetType as R, parseDialogue as k } from "@doodle-engine/core";
11
+ import J from "prompts";
12
+ function C(t, o) {
13
+ const s = [];
14
+ for (const r of Object.values(t.dialogues)) {
15
+ const n = o.get(r.id) || `dialogue:${r.id}`;
16
+ s.push(...Q(r, n));
17
17
  }
18
- for (const o of Object.values(n.characters))
19
- if (o.dialogue && !n.dialogues[o.dialogue]) {
20
- const t = a.get(o.id) || `character:${o.id}`;
21
- r.push({
22
- file: t,
23
- message: `Character "${o.id}" references non-existent dialogue "${o.dialogue}"`,
24
- suggestion: `Create dialogue "${o.dialogue}" or fix the reference`
18
+ for (const r of Object.values(t.characters))
19
+ if (r.dialogue && !t.dialogues[r.dialogue]) {
20
+ const n = o.get(r.id) || `character:${r.id}`;
21
+ s.push({
22
+ file: n,
23
+ message: `Character "${r.id}" references non-existent dialogue "${r.dialogue}"`,
24
+ suggestion: `Create dialogue "${r.dialogue}" or fix the reference`
25
25
  });
26
26
  }
27
- return r.push(...W(n, a)), r;
27
+ return s.push(...K(t, o)), s;
28
28
  }
29
- function B(n, a) {
30
- const r = [], o = /* @__PURE__ */ new Set();
31
- for (const t of n.nodes)
32
- o.has(t.id) && r.push({
33
- file: a,
34
- message: `Duplicate node ID "${t.id}"`,
29
+ function Q(t, o) {
30
+ const s = [], r = /* @__PURE__ */ new Set();
31
+ for (const n of t.nodes)
32
+ r.has(n.id) && s.push({
33
+ file: o,
34
+ message: `Duplicate node ID "${n.id}"`,
35
35
  suggestion: "Node IDs must be unique within a dialogue"
36
- }), o.add(t.id);
37
- o.has(n.startNode) || r.push({
38
- file: a,
39
- message: `Start node "${n.startNode}" not found`,
40
- suggestion: `Add a NODE ${n.startNode} or fix the startNode reference`
36
+ }), r.add(n.id);
37
+ r.has(t.startNode) || s.push({
38
+ file: o,
39
+ message: `Start node "${t.startNode}" not found`,
40
+ suggestion: `Add a NODE ${t.startNode} or fix the startNode reference`
41
41
  });
42
- for (const t of n.nodes)
43
- r.push(...L(t, o, a));
44
- return r;
42
+ for (const n of t.nodes)
43
+ s.push(...Y(n, r, o));
44
+ return s;
45
45
  }
46
- function L(n, a, r) {
47
- const o = [];
48
- if (n.next && !a.has(n.next) && o.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
- a.has(t.next) || o.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
- }), o.push(...v(t.condition, n.id, r));
59
- for (const t of n.choices) {
60
- if (!t.effects?.some(
46
+ function Y(t, o, s) {
47
+ const r = [];
48
+ if (t.next && !o.has(t.next) && r.push({
49
+ file: s,
50
+ message: `Node "${t.id}" GOTO "${t.next}" points to non-existent node`,
51
+ suggestion: `Add NODE ${t.next} or fix the GOTO target`
52
+ }), t.conditionalNext)
53
+ for (const n of t.conditionalNext)
54
+ o.has(n.next) || r.push({
55
+ file: s,
56
+ message: `Node "${t.id}" IF block GOTO "${n.next}" points to non-existent node`,
57
+ suggestion: `Add NODE ${n.next} or fix the GOTO target`
58
+ }), r.push(...O(n.condition, t.id, s));
59
+ for (const n of t.choices) {
60
+ if (!n.effects?.some(
61
61
  (e) => e.type === "endDialogue" || e.type === "goToLocation" || e.type === "startDialogue"
62
- ) && t.next && !a.has(t.next) && o.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
- o.push(...v(e, n.id, r));
69
- if (t.effects)
70
- for (const e of t.effects)
71
- o.push(...C(e, n.id, r));
62
+ ) && n.next && !o.has(n.next) && r.push({
63
+ file: s,
64
+ message: `Node "${t.id}" choice "${n.id}" GOTO "${n.next}" points to non-existent node`,
65
+ suggestion: `Add NODE ${n.next} or fix the GOTO target`
66
+ }), n.conditions)
67
+ for (const e of n.conditions)
68
+ r.push(...O(e, t.id, s));
69
+ if (n.effects)
70
+ for (const e of n.effects)
71
+ r.push(...A(e, t.id, s));
72
72
  }
73
- if (n.conditions)
74
- for (const t of n.conditions)
75
- o.push(...v(t, n.id, r));
76
- if (n.effects)
77
- for (const t of n.effects)
78
- o.push(...C(t, n.id, r));
79
- return o;
73
+ if (t.conditions)
74
+ for (const n of t.conditions)
75
+ r.push(...O(n, t.id, s));
76
+ if (t.effects)
77
+ for (const n of t.effects)
78
+ r.push(...A(n, t.id, s));
79
+ return r;
80
80
  }
81
- const P = {
81
+ const z = {
82
82
  hasFlag: ["flag"],
83
83
  notFlag: ["flag"],
84
84
  hasItem: ["itemId"],
@@ -94,7 +94,7 @@ const P = {
94
94
  relationshipBelow: ["characterId", "value"],
95
95
  itemAt: ["itemId", "locationId"],
96
96
  roll: ["min", "max", "threshold"]
97
- }, U = {
97
+ }, V = {
98
98
  setFlag: ["flag"],
99
99
  clearFlag: ["flag"],
100
100
  setVariable: ["variable", "value"],
@@ -123,142 +123,188 @@ const P = {
123
123
  showInterlude: ["interludeId"],
124
124
  roll: ["variable", "min", "max"]
125
125
  };
126
- function v(n, a, r) {
127
- const o = [];
128
- if (!n.type)
129
- return o.push({
130
- file: r,
131
- message: `Node "${a}" has condition with missing type`
132
- }), o;
133
- if (n.type === "timeIs")
134
- return n.hour === void 0 && n.day === void 0 && o.push({
135
- file: r,
136
- message: `Node "${a}" condition "timeIs" must have at least one of "hour" or "day" argument`
137
- }), o;
138
- const t = P[n.type];
139
- if (!t)
140
- return o;
141
- for (const i of t)
142
- (n[i] === void 0 || n[i] === null || n[i] === "") && o.push({
143
- file: r,
144
- message: `Node "${a}" condition "${n.type}" missing required "${i}" argument`
126
+ function O(t, o, s) {
127
+ const r = [];
128
+ if (!t.type)
129
+ return r.push({
130
+ file: s,
131
+ message: `Node "${o}" has condition with missing type`
132
+ }), r;
133
+ if (t.type === "timeIs")
134
+ return t.hour === void 0 && t.day === void 0 && r.push({
135
+ file: s,
136
+ message: `Node "${o}" condition "timeIs" must have at least one of "hour" or "day" argument`
137
+ }), r;
138
+ const n = z[t.type];
139
+ if (!n)
140
+ return r;
141
+ for (const a of n)
142
+ (t[a] === void 0 || t[a] === null || t[a] === "") && r.push({
143
+ file: s,
144
+ message: `Node "${o}" condition "${t.type}" missing required "${a}" argument`
145
145
  });
146
- return o;
146
+ return r;
147
147
  }
148
- function C(n, a, r) {
149
- const o = [];
150
- if (!n.type)
151
- return o.push({
152
- file: r,
153
- message: `Node "${a}" has effect with missing type`
154
- }), o;
155
- const t = U[n.type];
156
- if (!t)
157
- return o;
158
- for (const i of t)
159
- (n[i] === void 0 || n[i] === null || n[i] === "") && o.push({
160
- file: r,
161
- message: `Node "${a}" effect "${n.type}" missing required "${i}" argument`
148
+ function A(t, o, s) {
149
+ const r = [];
150
+ if (!t.type)
151
+ return r.push({
152
+ file: s,
153
+ message: `Node "${o}" has effect with missing type`
154
+ }), r;
155
+ const n = V[t.type];
156
+ if (!n)
157
+ return r;
158
+ for (const a of n)
159
+ (t[a] === void 0 || t[a] === null || t[a] === "") && r.push({
160
+ file: s,
161
+ message: `Node "${o}" effect "${t.type}" missing required "${a}" argument`
162
162
  });
163
- return o;
163
+ return r;
164
164
  }
165
- function W(n, a) {
166
- const r = [], o = /* @__PURE__ */ new Set();
167
- for (const e of Object.values(n.locales))
168
- for (const c of Object.keys(e))
169
- o.add(c);
170
- const t = (e) => e.startsWith("@"), i = (e, c, l) => {
165
+ function K(t, o) {
166
+ const s = [], r = /* @__PURE__ */ new Set();
167
+ for (const e of Object.values(t.locales))
168
+ for (const i of Object.keys(e))
169
+ r.add(i);
170
+ const n = (e) => e.startsWith("@"), a = (e, i, l) => {
171
171
  const d = e.slice(1);
172
- if (!o.has(d)) {
173
- const u = a.get(c) || `${l}:${c}`;
174
- r.push({
172
+ if (!r.has(d)) {
173
+ const u = o.get(i) || `${l}:${i}`;
174
+ s.push({
175
175
  file: u,
176
176
  message: `Localization key "${e}" not found in any locale file`,
177
177
  suggestion: `Add "${d}: ..." to your locale files`
178
178
  });
179
179
  }
180
180
  };
181
- for (const e of Object.values(n.locations))
182
- t(e.name) && i(e.name, e.id, "location"), t(e.description) && i(e.description, e.id, "location");
183
- for (const e of Object.values(n.characters))
184
- t(e.name) && i(e.name, e.id, "character"), t(e.biography) && i(e.biography, e.id, "character");
185
- for (const e of Object.values(n.items))
186
- t(e.name) && i(e.name, e.id, "item"), t(e.description) && i(e.description, e.id, "item");
187
- for (const e of Object.values(n.quests)) {
188
- t(e.name) && i(e.name, e.id, "quest"), t(e.description) && i(e.description, e.id, "quest");
189
- for (const c of e.stages)
190
- t(c.description) && i(c.description, e.id, "quest");
181
+ for (const e of Object.values(t.locations))
182
+ n(e.name) && a(e.name, e.id, "location"), n(e.description) && a(e.description, e.id, "location");
183
+ for (const e of Object.values(t.characters))
184
+ n(e.name) && a(e.name, e.id, "character"), n(e.biography) && a(e.biography, e.id, "character");
185
+ for (const e of Object.values(t.items))
186
+ n(e.name) && a(e.name, e.id, "item"), n(e.description) && a(e.description, e.id, "item");
187
+ for (const e of Object.values(t.quests)) {
188
+ n(e.name) && a(e.name, e.id, "quest"), n(e.description) && a(e.description, e.id, "quest");
189
+ for (const i of e.stages)
190
+ n(i.description) && a(i.description, e.id, "quest");
191
191
  }
192
- for (const e of Object.values(n.journalEntries))
193
- t(e.title) && i(e.title, e.id, "journal"), t(e.text) && i(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) && i(c.text, e.id, "dialogue");
197
- for (const l of c.choices)
198
- t(l.text) && i(l.text, e.id, "dialogue");
192
+ for (const e of Object.values(t.journalEntries))
193
+ n(e.title) && a(e.title, e.id, "journal"), n(e.text) && a(e.text, e.id, "journal");
194
+ for (const e of Object.values(t.dialogues))
195
+ for (const i of e.nodes) {
196
+ n(i.text) && a(i.text, e.id, "dialogue");
197
+ for (const l of i.choices)
198
+ n(l.text) && a(l.text, e.id, "dialogue");
199
199
  }
200
- for (const e of Object.values(n.interludes))
201
- t(e.text) && i(e.text, e.id, "interlude");
202
- return r;
200
+ for (const e of Object.values(t.interludes))
201
+ n(e.text) && a(e.text, e.id, "interlude");
202
+ return s;
203
203
  }
204
- function I(n) {
205
- if (n.length === 0) {
206
- console.log(s.green("✓ No validation errors"));
204
+ function I(t) {
205
+ if (t.length === 0) {
206
+ console.log(c.green("✓ No validation errors"));
207
207
  return;
208
208
  }
209
- console.log(s.red(`
210
- ✗ Found ${n.length} validation error${n.length === 1 ? "" : "s"}:
209
+ console.log(c.red(`
210
+ ✗ Found ${t.length} validation error${t.length === 1 ? "" : "s"}:
211
211
  `));
212
- for (const a of n)
213
- console.log(s.bold(a.file) + (a.line ? `:${a.line}` : "")), console.log(" " + s.red(a.message)), a.suggestion && console.log(" " + s.dim(a.suggestion)), console.log();
212
+ for (const o of t)
213
+ console.log(c.bold(o.file) + (o.line ? `:${o.line}` : "")), console.log(" " + c.red(o.message)), o.suggestion && console.log(" " + c.dim(o.suggestion)), console.log();
214
214
  }
215
- const R = "ðŸū", J = "âœĻ", Q = "✏ïļ", Y = "➕";
216
- async function V() {
217
- const n = process.cwd(), a = h(n, "content");
218
- console.log(""), console.log(s.bold.magenta(` ${R} Doodle Engine Dev Server ${R}`)), console.log("");
219
- const r = {
215
+ async function q(t, o, s, r, n = Date.now().toString()) {
216
+ const { shell: a, game: e } = W(s, r);
217
+ async function i(g) {
218
+ const _ = h(o, g.startsWith("/") ? g.slice(1) : g);
219
+ try {
220
+ return (await B(_)).size;
221
+ } catch {
222
+ return;
223
+ }
224
+ }
225
+ const l = await Promise.all(
226
+ a.map(async (g) => ({
227
+ path: g,
228
+ type: R(g),
229
+ size: await i(g),
230
+ tier: 1
231
+ }))
232
+ ), d = await Promise.all(
233
+ e.map(async (g) => ({
234
+ path: g,
235
+ type: R(g),
236
+ size: await i(g),
237
+ tier: 2
238
+ }))
239
+ ), u = l.reduce((g, _) => g + (_.size ?? 0), 0), m = d.reduce((g, _) => g + (_.size ?? 0), 0);
240
+ return {
241
+ version: n,
242
+ shell: l,
243
+ game: d,
244
+ shellSize: u,
245
+ totalSize: u + m
246
+ };
247
+ }
248
+ const S = "ðŸū", X = "âœĻ", Z = "✏ïļ", ee = "➕";
249
+ async function te() {
250
+ const t = process.cwd(), o = h(t, "content");
251
+ console.log(""), console.log(c.bold.magenta(` ${S} Doodle Engine Dev Server ${S}`)), console.log("");
252
+ const s = {
220
253
  name: "doodle-content-loader",
221
- configureServer(t) {
222
- t.middlewares.use("/api/content", async (d, u) => {
254
+ configureServer(n) {
255
+ n.middlewares.use("/api/content", async (d, u) => {
223
256
  try {
224
- const m = await K(a);
257
+ const m = await j(o);
225
258
  u.setHeader("Content-Type", "application/json"), u.end(JSON.stringify(m));
226
259
  } catch (m) {
227
- console.error(s.red(" Error loading content:"), m), u.statusCode = 500, u.end(JSON.stringify({ error: "Failed to load content" }));
260
+ console.error(c.red(" Error loading content:"), m), u.statusCode = 500, u.end(JSON.stringify({ error: "Failed to load content" }));
261
+ }
262
+ }), n.middlewares.use("/api/manifest", async (d, u) => {
263
+ try {
264
+ const { registry: m, config: g } = await j(o), _ = await q(
265
+ h(t, "assets"),
266
+ t,
267
+ m,
268
+ g,
269
+ "dev"
270
+ );
271
+ u.setHeader("Content-Type", "application/json"), u.end(JSON.stringify(_));
272
+ } catch (m) {
273
+ console.error(c.red(" Error generating manifest:"), m), u.statusCode = 500, u.end(JSON.stringify({ error: "Failed to generate manifest" }));
228
274
  }
229
275
  });
230
- const i = H(a, {
276
+ const a = L(o, {
231
277
  ignored: /(^|[\/\\])\../,
232
278
  persistent: !0
233
279
  });
234
280
  let e = !1;
235
- i.on("ready", () => {
281
+ a.on("ready", () => {
236
282
  e = !0;
237
283
  });
238
- let c = null;
284
+ let i = null;
239
285
  const l = (d) => {
240
- c && clearTimeout(c), c = setTimeout(async () => {
241
- c = null, console.log(d), await z(a), t.ws.send({ type: "full-reload", path: "*" });
286
+ i && clearTimeout(i), i = setTimeout(async () => {
287
+ i = null, console.log(d), await ne(o), n.ws.send({ type: "full-reload", path: "*" });
242
288
  }, 50);
243
289
  };
244
- i.on("change", (d) => {
245
- l(s.yellow(` ${Q} Content changed: ${d}`));
246
- }), i.on("add", (d) => {
247
- e && l(s.green(` ${Y} Content added: ${d}`));
290
+ a.on("change", (d) => {
291
+ l(c.yellow(` ${Z} Content changed: ${d}`));
292
+ }), a.on("add", (d) => {
293
+ e && l(c.green(` ${ee} Content added: ${d}`));
248
294
  });
249
295
  }
250
- }, o = await q({
251
- root: n,
252
- plugins: [S(), r],
296
+ }, r = await F({
297
+ root: t,
298
+ plugins: [G(), s],
253
299
  server: {
254
300
  port: 3e3,
255
301
  open: !0
256
302
  }
257
303
  });
258
- await o.listen(), o.printUrls(), console.log(""), console.log(s.dim(` ${J} Watching content files for changes...`)), console.log("");
304
+ await r.listen(), r.printUrls(), console.log(""), console.log(c.dim(` ${X} Watching content files for changes...`)), console.log("");
259
305
  }
260
- async function K(n) {
261
- const a = {
306
+ async function j(t) {
307
+ const o = {
262
308
  locations: {},
263
309
  characters: {},
264
310
  items: {},
@@ -269,8 +315,8 @@ async function K(n) {
269
315
  interludes: {},
270
316
  locales: {}
271
317
  };
272
- let r = null;
273
- const o = [
318
+ let s = null;
319
+ const r = [
274
320
  { dir: "locations", key: "locations" },
275
321
  { dir: "characters", key: "characters" },
276
322
  { dir: "items", key: "items" },
@@ -279,41 +325,41 @@ async function K(n) {
279
325
  { dir: "journal", key: "journalEntries" },
280
326
  { dir: "interludes", key: "interludes" }
281
327
  ];
282
- for (const { dir: t, key: i } of o) {
283
- const e = h(n, t);
328
+ for (const { dir: n, key: a } of r) {
329
+ const e = h(t, n);
284
330
  try {
285
- const c = await p(e);
286
- for (const l of c)
287
- if (g(l) === ".yaml" || g(l) === ".yml") {
288
- const d = h(e, l), u = await f(d, "utf-8"), m = b(u);
289
- m && m.id && (a[i][m.id] = m);
331
+ const i = await b(e);
332
+ for (const l of i)
333
+ if (f(l) === ".yaml" || f(l) === ".yml") {
334
+ const d = h(e, l), u = await p(d, "utf-8"), m = y(u);
335
+ m && m.id && (o[a][m.id] = m);
290
336
  }
291
337
  } catch {
292
338
  }
293
339
  }
294
340
  try {
295
- const t = h(n, "locales"), i = await p(t);
296
- for (const e of i)
297
- if (g(e) === ".yaml" || g(e) === ".yml") {
298
- const c = h(t, e), l = await f(c, "utf-8"), d = b(l), u = e.replace(/\.(yaml|yml)$/, "");
299
- a.locales[u] = d ?? {};
341
+ const n = h(t, "locales"), a = await b(n);
342
+ for (const e of a)
343
+ if (f(e) === ".yaml" || f(e) === ".yml") {
344
+ const i = h(n, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
345
+ o.locales[u] = d ?? {};
300
346
  }
301
347
  } catch {
302
348
  }
303
349
  try {
304
- const t = h(n, "dialogues"), i = await p(t);
305
- for (const e of i)
306
- if (g(e) === ".dlg") {
307
- const c = h(t, e), l = await f(c, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
308
- a.dialogues[u.id] = u;
350
+ const n = h(t, "dialogues"), a = await b(n);
351
+ for (const e of a)
352
+ if (f(e) === ".dlg") {
353
+ const i = h(n, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
354
+ o.dialogues[u.id] = u;
309
355
  }
310
356
  } catch {
311
357
  }
312
358
  try {
313
- const t = h(n, "game.yaml"), i = await f(t, "utf-8");
314
- r = b(i);
359
+ const n = h(t, "game.yaml"), a = await p(n, "utf-8");
360
+ s = y(a);
315
361
  } catch {
316
- console.warn(s.yellow(" No game.yaml found, using defaults")), r = {
362
+ console.warn(c.yellow(" No game.yaml found, using defaults")), s = {
317
363
  startLocation: "tavern",
318
364
  startTime: { day: 1, hour: 8 },
319
365
  startFlags: {},
@@ -321,18 +367,18 @@ async function K(n) {
321
367
  startInventory: []
322
368
  };
323
369
  }
324
- return { registry: a, config: r };
370
+ return { registry: o, config: s };
325
371
  }
326
- async function z(n) {
372
+ async function ne(t) {
327
373
  try {
328
- const { registry: a, fileMap: r } = await X(n), o = N(a, r);
329
- o.length > 0 && (console.log(""), I(o), console.log(""));
330
- } catch (a) {
331
- console.error(s.red(" Error running validation:"), a);
374
+ const { registry: o, fileMap: s } = await oe(t), r = C(o, s);
375
+ r.length > 0 && (console.log(""), I(r), console.log(""));
376
+ } catch (o) {
377
+ console.error(c.red(" Error running validation:"), o);
332
378
  }
333
379
  }
334
- async function X(n) {
335
- const a = {
380
+ async function oe(t) {
381
+ const o = {
336
382
  locations: {},
337
383
  characters: {},
338
384
  items: {},
@@ -342,7 +388,7 @@ async function X(n) {
342
388
  journalEntries: {},
343
389
  interludes: {},
344
390
  locales: {}
345
- }, r = /* @__PURE__ */ new Map(), o = [
391
+ }, s = /* @__PURE__ */ new Map(), r = [
346
392
  { dir: "locations", key: "locations" },
347
393
  { dir: "characters", key: "characters" },
348
394
  { dir: "items", key: "items" },
@@ -351,66 +397,153 @@ async function X(n) {
351
397
  { dir: "journal", key: "journalEntries" },
352
398
  { dir: "interludes", key: "interludes" }
353
399
  ];
354
- for (const { dir: t, key: i } of o) {
355
- const e = h(n, t);
400
+ for (const { dir: n, key: a } of r) {
401
+ const e = h(t, n);
356
402
  try {
357
- const c = await p(e);
358
- for (const l of c)
359
- if (g(l) === ".yaml" || g(l) === ".yml") {
360
- const d = h(e, l), u = await f(d, "utf-8"), m = b(u);
361
- m && m.id && (a[i][m.id] = m, r.set(m.id, y(process.cwd(), d)));
403
+ const i = await b(e);
404
+ for (const l of i)
405
+ if (f(l) === ".yaml" || f(l) === ".yml") {
406
+ const d = h(e, l), u = await p(d, "utf-8"), m = y(u);
407
+ m && m.id && (o[a][m.id] = m, s.set(m.id, E(process.cwd(), d)));
362
408
  }
363
409
  } catch {
364
410
  }
365
411
  }
366
412
  try {
367
- const t = h(n, "locales"), i = await p(t);
368
- for (const e of i)
369
- if (g(e) === ".yaml" || g(e) === ".yml") {
370
- const c = h(t, e), l = await f(c, "utf-8"), d = b(l), u = e.replace(/\.(yaml|yml)$/, "");
371
- a.locales[u] = d ?? {};
413
+ const n = h(t, "locales"), a = await b(n);
414
+ for (const e of a)
415
+ if (f(e) === ".yaml" || f(e) === ".yml") {
416
+ const i = h(n, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
417
+ o.locales[u] = d ?? {};
372
418
  }
373
419
  } catch {
374
420
  }
375
421
  try {
376
- const t = h(n, "dialogues"), i = await p(t);
377
- for (const e of i)
378
- if (g(e) === ".dlg") {
379
- const c = h(t, e), l = await f(c, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
380
- a.dialogues[u.id] = u, r.set(u.id, y(process.cwd(), c));
422
+ const n = h(t, "dialogues"), a = await b(n);
423
+ for (const e of a)
424
+ if (f(e) === ".dlg") {
425
+ const i = h(n, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
426
+ o.dialogues[u.id] = u, s.set(u.id, E(process.cwd(), i));
381
427
  }
382
428
  } catch {
383
429
  }
384
- return { registry: a, fileMap: r };
430
+ return { registry: o, fileMap: s };
385
431
  }
386
- async function Z() {
387
- const n = process.cwd(), a = h(n, "content");
388
- console.log(""), console.log(s.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(s.dim("Validating content..."));
389
- let r;
432
+ function ae(t) {
433
+ const o = `doodle-engine-assets-${t.version}`, s = [
434
+ ...t.shell.map((n) => n.path),
435
+ ...t.game.map((n) => n.path)
436
+ ], r = JSON.stringify(s, null, 2);
437
+ return `/**
438
+ * Doodle Engine Service Worker
439
+ * Generated at build time — do not edit manually.
440
+ * Cache version: ${t.version}
441
+ */
442
+
443
+ const CACHE_NAME = ${JSON.stringify(o)};
444
+ const PRECACHE_URLS = ${r};
445
+
446
+ // Install: precache all manifest assets
447
+ self.addEventListener('install', (event) => {
448
+ event.waitUntil(
449
+ caches.open(CACHE_NAME).then((cache) => {
450
+ // Cache assets individually so one failure doesn't block everything
451
+ return Promise.allSettled(
452
+ PRECACHE_URLS.map((url) =>
453
+ cache.add(url).catch((err) => {
454
+ console.warn('[sw] Failed to precache:', url, err);
455
+ })
456
+ )
457
+ );
458
+ }).then(() => self.skipWaiting())
459
+ );
460
+ });
461
+
462
+ // Activate: clean up old caches
463
+ self.addEventListener('activate', (event) => {
464
+ event.waitUntil(
465
+ caches.keys().then((keys) =>
466
+ Promise.all(
467
+ keys
468
+ .filter((key) => key.startsWith('doodle-engine-assets-') && key !== CACHE_NAME)
469
+ .map((key) => caches.delete(key))
470
+ )
471
+ ).then(() => self.clients.claim())
472
+ );
473
+ });
474
+
475
+ // Fetch: cache-first for precached assets, network-first for everything else
476
+ self.addEventListener('fetch', (event) => {
477
+ const url = new URL(event.request.url);
478
+
479
+ // Only handle same-origin GET requests
480
+ if (event.request.method !== 'GET' || url.origin !== self.location.origin) {
481
+ return;
482
+ }
483
+
484
+ // Skip API and non-asset requests — serve from network
485
+ if (url.pathname.startsWith('/api/')) {
486
+ return;
487
+ }
488
+
489
+ event.respondWith(
490
+ caches.match(event.request).then((cached) => {
491
+ if (cached) return cached;
492
+
493
+ return fetch(event.request).then((response) => {
494
+ // Cache successful responses for precached paths
495
+ if (response.ok && PRECACHE_URLS.includes(url.pathname)) {
496
+ caches.open(CACHE_NAME).then((cache) => {
497
+ cache.put(event.request, response.clone());
498
+ });
499
+ }
500
+ return response;
501
+ });
502
+ })
503
+ );
504
+ });
505
+ `;
506
+ }
507
+ async function re() {
508
+ const t = process.cwd(), o = h(t, "content");
509
+ console.log(""), console.log(c.bold.magenta("🐕 Building Doodle Engine game...")), console.log(""), console.log(c.dim("Validating content..."));
510
+ let s, r, n;
390
511
  try {
391
- const { registry: o, fileMap: t, config: i } = await ee(a), e = N(o, t);
392
- I(e), e.length > 0 && (console.log(s.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), r = { registry: o, config: i };
393
- } catch (o) {
394
- console.error(s.red("Error loading content:"), o), process.exit(1);
512
+ const a = await se(o);
513
+ r = a.registry, n = a.config;
514
+ const { fileMap: e } = a, i = C(r, e);
515
+ I(i), i.length > 0 && (console.log(c.red("Build failed due to validation errors.")), console.log(""), process.exit(1)), s = { registry: r, config: n };
516
+ } catch (a) {
517
+ console.error(c.red("Error loading content:"), a), process.exit(1);
395
518
  }
396
519
  console.log("");
397
520
  try {
398
- await $({
399
- root: n,
400
- plugins: [S()],
521
+ await P({
522
+ root: t,
523
+ plugins: [G()],
401
524
  build: {
402
525
  outDir: "dist",
403
526
  emptyOutDir: !0
404
527
  }
405
528
  });
406
- const o = h(n, "dist", "api");
407
- await D(o, { recursive: !0 }), await w(h(o, "content"), JSON.stringify(r)), console.log(""), console.log(s.green("✅ Build complete! Output in dist/")), console.log(""), console.log("To preview the build:"), console.log(s.dim(" yarn preview")), console.log("");
408
- } catch (o) {
409
- console.error(s.red("Build failed:"), o), process.exit(1);
529
+ const a = h(t, "dist"), e = t;
530
+ console.log(c.dim("Generating asset manifest..."));
531
+ const i = await q(
532
+ h(t, "assets"),
533
+ e,
534
+ r,
535
+ n,
536
+ Date.now().toString()
537
+ ), l = h(a, "api");
538
+ await T(l, { recursive: !0 }), await w(h(l, "content"), JSON.stringify(s)), await w(h(l, "manifest"), JSON.stringify(i)), await w(h(a, "asset-manifest.json"), JSON.stringify(i, null, 2)), console.log(c.dim("Generating service worker..."));
539
+ const d = ae(i);
540
+ await w(h(a, "sw.js"), d), 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("");
541
+ } catch (a) {
542
+ console.error(c.red("Build failed:"), a), process.exit(1);
410
543
  }
411
544
  }
412
- async function ee(n) {
413
- const a = {
545
+ async function se(t) {
546
+ const o = {
414
547
  locations: {},
415
548
  characters: {},
416
549
  items: {},
@@ -420,7 +553,7 @@ async function ee(n) {
420
553
  journalEntries: {},
421
554
  interludes: {},
422
555
  locales: {}
423
- }, r = /* @__PURE__ */ new Map(), o = [
556
+ }, s = /* @__PURE__ */ new Map(), r = [
424
557
  { dir: "locations", key: "locations" },
425
558
  { dir: "characters", key: "characters" },
426
559
  { dir: "items", key: "items" },
@@ -429,57 +562,57 @@ async function ee(n) {
429
562
  { dir: "journal", key: "journalEntries" },
430
563
  { dir: "interludes", key: "interludes" }
431
564
  ];
432
- for (const { dir: i, key: e } of o) {
433
- const c = h(n, i);
565
+ for (const { dir: a, key: e } of r) {
566
+ const i = h(t, a);
434
567
  try {
435
- const l = await p(c);
568
+ const l = await b(i);
436
569
  for (const d of l)
437
- if (g(d) === ".yaml" || g(d) === ".yml") {
438
- const u = h(c, d), m = await f(u, "utf-8"), E = b(m);
439
- E && E.id && (a[e][E.id] = E, r.set(E.id, y(process.cwd(), u)));
570
+ if (f(d) === ".yaml" || f(d) === ".yml") {
571
+ const u = h(i, d), m = await p(u, "utf-8"), g = y(m);
572
+ g && g.id && (o[e][g.id] = g, s.set(g.id, E(process.cwd(), u)));
440
573
  }
441
574
  } catch {
442
575
  }
443
576
  }
444
577
  try {
445
- const i = h(n, "locales"), e = await p(i);
446
- for (const c of e)
447
- if (g(c) === ".yaml" || g(c) === ".yml") {
448
- const l = h(i, c), d = await f(l, "utf-8"), u = b(d), m = c.replace(/\.(yaml|yml)$/, "");
449
- a.locales[m] = u ?? {};
578
+ const a = h(t, "locales"), e = await b(a);
579
+ for (const i of e)
580
+ if (f(i) === ".yaml" || f(i) === ".yml") {
581
+ const l = h(a, i), d = await p(l, "utf-8"), u = y(d), m = i.replace(/\.(yaml|yml)$/, "");
582
+ o.locales[m] = u ?? {};
450
583
  }
451
584
  } catch {
452
585
  }
453
586
  try {
454
- const i = h(n, "dialogues"), e = await p(i);
455
- for (const c of e)
456
- if (g(c) === ".dlg") {
457
- const l = h(i, c), d = await f(l, "utf-8"), u = c.replace(".dlg", ""), m = k(d, u);
458
- a.dialogues[m.id] = m, r.set(m.id, y(process.cwd(), l));
587
+ const a = h(t, "dialogues"), e = await b(a);
588
+ for (const i of e)
589
+ if (f(i) === ".dlg") {
590
+ const l = h(a, i), d = await p(l, "utf-8"), u = i.replace(".dlg", ""), m = k(d, u);
591
+ o.dialogues[m.id] = m, s.set(m.id, E(process.cwd(), l));
459
592
  }
460
593
  } catch {
461
594
  }
462
- let t = null;
595
+ let n = null;
463
596
  try {
464
- const i = h(n, "game.yaml"), e = await f(i, "utf-8");
465
- t = b(e);
597
+ const a = h(t, "game.yaml"), e = await p(a, "utf-8");
598
+ n = y(e);
466
599
  } catch {
467
- t = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
600
+ n = { id: "game", startLocation: "", startTime: { day: 1, hour: 8 }, startFlags: {}, startVariables: {}, startInventory: [] };
468
601
  }
469
- return { registry: a, fileMap: r, config: t };
602
+ return { registry: o, fileMap: s, config: n };
470
603
  }
471
- async function ne() {
472
- const n = process.cwd(), a = h(n, "content");
473
- console.log(""), console.log(s.bold.magenta("ðŸū Validating Doodle Engine content...")), console.log("");
604
+ async function ie() {
605
+ const t = process.cwd(), o = h(t, "content");
606
+ console.log(""), console.log(c.bold.magenta("ðŸū Validating Doodle Engine content...")), console.log("");
474
607
  try {
475
- const { registry: r, fileMap: o } = await te(a), t = N(r, o);
476
- I(t), t.length > 0 && process.exit(1);
477
- } catch (r) {
478
- console.error(s.red("Error loading content:"), r), process.exit(1);
608
+ const { registry: s, fileMap: r } = await ce(o), n = C(s, r);
609
+ I(n), n.length > 0 && process.exit(1);
610
+ } catch (s) {
611
+ console.error(c.red("Error loading content:"), s), process.exit(1);
479
612
  }
480
613
  }
481
- async function te(n) {
482
- const a = {
614
+ async function ce(t) {
615
+ const o = {
483
616
  locations: {},
484
617
  characters: {},
485
618
  items: {},
@@ -488,7 +621,7 @@ async function te(n) {
488
621
  quests: {},
489
622
  journalEntries: {},
490
623
  locales: {}
491
- }, r = /* @__PURE__ */ new Map(), o = [
624
+ }, s = /* @__PURE__ */ new Map(), r = [
492
625
  { dir: "locations", key: "locations" },
493
626
  { dir: "characters", key: "characters" },
494
627
  { dir: "items", key: "items" },
@@ -496,43 +629,43 @@ async function te(n) {
496
629
  { dir: "quests", key: "quests" },
497
630
  { dir: "journal", key: "journalEntries" }
498
631
  ];
499
- for (const { dir: t, key: i } of o) {
500
- const e = h(n, t);
632
+ for (const { dir: n, key: a } of r) {
633
+ const e = h(t, n);
501
634
  try {
502
- const c = await p(e);
503
- for (const l of c)
504
- if (g(l) === ".yaml" || g(l) === ".yml") {
505
- const d = h(e, l), u = await f(d, "utf-8"), m = b(u);
506
- m && m.id && (a[i][m.id] = m, r.set(m.id, y(process.cwd(), d)));
635
+ const i = await b(e);
636
+ for (const l of i)
637
+ if (f(l) === ".yaml" || f(l) === ".yml") {
638
+ const d = h(e, l), u = await p(d, "utf-8"), m = y(u);
639
+ m && m.id && (o[a][m.id] = m, s.set(m.id, E(process.cwd(), d)));
507
640
  }
508
641
  } catch {
509
642
  }
510
643
  }
511
644
  try {
512
- const t = h(n, "locales"), i = await p(t);
513
- for (const e of i)
514
- if (g(e) === ".yaml" || g(e) === ".yml") {
515
- const c = h(t, e), l = await f(c, "utf-8"), d = b(l), u = e.replace(/\.(yaml|yml)$/, "");
516
- a.locales[u] = d ?? {};
645
+ const n = h(t, "locales"), a = await b(n);
646
+ for (const e of a)
647
+ if (f(e) === ".yaml" || f(e) === ".yml") {
648
+ const i = h(n, e), l = await p(i, "utf-8"), d = y(l), u = e.replace(/\.(yaml|yml)$/, "");
649
+ o.locales[u] = d ?? {};
517
650
  }
518
651
  } catch {
519
652
  }
520
653
  try {
521
- const t = h(n, "dialogues"), i = await p(t);
522
- for (const e of i)
523
- if (g(e) === ".dlg") {
524
- const c = h(t, e), l = await f(c, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
525
- a.dialogues[u.id] = u, r.set(u.id, y(process.cwd(), c));
654
+ const n = h(t, "dialogues"), a = await b(n);
655
+ for (const e of a)
656
+ if (f(e) === ".dlg") {
657
+ const i = h(n, e), l = await p(i, "utf-8"), d = e.replace(".dlg", ""), u = k(l, d);
658
+ o.dialogues[u.id] = u, s.set(u.id, E(process.cwd(), i));
526
659
  }
527
660
  } catch {
528
661
  }
529
- return { registry: a, fileMap: r };
662
+ return { registry: o, fileMap: s };
530
663
  }
531
- const oe = `node_modules
664
+ const le = `node_modules
532
665
  dist
533
666
  .DS_Store
534
667
  *.log
535
- `, ae = `<!doctype html>
668
+ `, de = `<!doctype html>
536
669
  <html lang="en">
537
670
  <head>
538
671
  <meta charset="UTF-8" />
@@ -544,7 +677,7 @@ dist
544
677
  <script type="module" src="/src/main.tsx"><\/script>
545
678
  </body>
546
679
  </html>
547
- `, re = `{
680
+ `, ue = `{
548
681
  "compilerOptions": {
549
682
  "target": "ES2024",
550
683
  "lib": ["ES2024", "DOM", "DOM.Iterable"],
@@ -557,25 +690,26 @@ dist
557
690
  "forceConsistentCasingInFileNames": true,
558
691
  "resolveJsonModule": true,
559
692
  "isolatedModules": true,
560
- "noEmit": true
693
+ "noEmit": true,
694
+ "types": ["vite/client"]
561
695
  },
562
696
  "include": ["src"]
563
697
  }
564
- `, ie = `id: bartender
698
+ `, he = `id: bartender
565
699
  name: "@character.bartender.name"
566
700
  biography: "@character.bartender.bio"
567
701
  portrait: ""
568
702
  location: tavern
569
703
  dialogue: bartender_greeting
570
704
  stats: {}
571
- `, se = `id: merchant
705
+ `, me = `id: merchant
572
706
  name: "@character.merchant.name"
573
707
  biography: "@character.merchant.bio"
574
708
  portrait: ""
575
709
  location: market
576
710
  dialogue: merchant_intro
577
711
  stats: {}
578
- `, ce = `# This dialogue is triggered by clicking the bartender character.
712
+ `, ge = `# This dialogue is triggered by clicking the bartender character.
579
713
  # SPEAKER: lines set who's talking — matched to character ID (case-insensitive).
580
714
  # Nodes can have multiple CHOICE blocks; REQUIRE hides a choice if the condition fails.
581
715
 
@@ -757,7 +891,7 @@ NODE work_done
757
891
  NODE farewell
758
892
  BARTENDER: @bartender.farewell
759
893
  END dialogue
760
- `, le = `# Standalone skill-check demo — demonstrates dice syntax in isolation.
894
+ `, fe = `# Standalone skill-check demo — demonstrates dice syntax in isolation.
761
895
  #
762
896
  # ROLL <variable> <min> <max> — rolls a random integer and stores it in a variable.
763
897
  # {varName} — in dialogue text, replaced with the variable's value.
@@ -821,7 +955,7 @@ NODE failure
821
955
  CHOICE @bluff.choice.walk_away
822
956
  END dialogue
823
957
  END
824
- `, de = `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
958
+ `, pe = `# One-time narrator intro for the market. Same pattern as tavern_intro.dlg.
825
959
 
826
960
  TRIGGER market
827
961
  REQUIRE notFlag seenMarketIntro
@@ -836,7 +970,7 @@ NODE start
836
970
  CHOICE @narrator.choice.look_around
837
971
  END dialogue
838
972
  END
839
- `, ue = `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
973
+ `, be = `# Merchant dialogue. Same speaker-line and CHOICE syntax as bartender_greeting.dlg.
840
974
  # The quest choices here demonstrate multi-stage quest gating with questAtStage.
841
975
 
842
976
  NODE start
@@ -939,7 +1073,7 @@ NODE about_market
939
1073
  NODE farewell
940
1074
  MERCHANT: @merchant.farewell
941
1075
  END dialogue
942
- `, he = `# This dialogue triggers automatically when the player enters the tavern.
1076
+ `, ye = `# This dialogue triggers automatically when the player enters the tavern.
943
1077
  # TRIGGER <locationId> fires on arrival. REQUIRE conditions guard the trigger.
944
1078
  # Use notFlag to make it a one-time intro.
945
1079
 
@@ -961,7 +1095,33 @@ NODE start
961
1095
  CHOICE @narrator.choice.look_around
962
1096
  END dialogue
963
1097
  END
964
- `, me = `# Game Configuration
1098
+ `, _e = `# Game Configuration
1099
+
1100
+ # Shell screen configuration
1101
+ # Uncomment and provide your own assets to customize the game shell.
1102
+ # The game will work without these — screens will use styled gradients and skip sounds.
1103
+ #
1104
+ # shell:
1105
+ # splash:
1106
+ # logo: /assets/images/studio-logo.png
1107
+ # background: /assets/images/splash-bg.jpg
1108
+ # sound: /assets/audio/sfx/splash-sting.ogg
1109
+ # duration: 2000
1110
+ #
1111
+ # loading:
1112
+ # background: /assets/images/loading-bg.jpg
1113
+ #
1114
+ # title:
1115
+ # logo: /assets/images/game-logo.png
1116
+ # background: /assets/images/title-bg.jpg
1117
+ # music: /assets/audio/music/title-theme.ogg
1118
+ #
1119
+ # uiSounds:
1120
+ # click: /assets/audio/sfx/ui-click.ogg
1121
+ # hover: /assets/audio/sfx/ui-hover.ogg
1122
+ # menuOpen: /assets/audio/sfx/menu-open.ogg
1123
+ # menuClose: /assets/audio/sfx/menu-close.ogg
1124
+
965
1125
  startLocation: tavern
966
1126
  startTime:
967
1127
  day: 1
@@ -972,7 +1132,7 @@ startVariables:
972
1132
  reputation: 0
973
1133
  _drinksBought: 0
974
1134
  startInventory: []
975
- `, ge = `id: chapter_one
1135
+ `, we = `id: chapter_one
976
1136
  # Background image shown fullscreen during the interlude.
977
1137
  # Replace with your own image in assets/images/.
978
1138
  background: /assets/images/banners/tavern_banner.jpg
@@ -1007,26 +1167,26 @@ triggerConditions:
1007
1167
  effects:
1008
1168
  - type: setFlag
1009
1169
  flag: seenChapterOne
1010
- `, fe = `id: old_coin
1170
+ `, Ee = `id: old_coin
1011
1171
  name: "@item.old_coin.name"
1012
1172
  description: "@item.old_coin.description"
1013
1173
  icon: ""
1014
1174
  image: ""
1015
1175
  location: tavern
1016
1176
  stats: {}
1017
- `, pe = `id: market_square
1177
+ `, ve = `id: market_square
1018
1178
  title: "@journal.market_square.title"
1019
1179
  text: "@journal.market_square.text"
1020
1180
  category: places
1021
- `, be = `id: odd_jobs_accepted
1181
+ `, ke = `id: odd_jobs_accepted
1022
1182
  title: "@journal.odd_jobs_accepted.title"
1023
1183
  text: "@journal.odd_jobs_accepted.text"
1024
1184
  category: quests
1025
- `, ye = `id: tavern_discovery
1185
+ `, Oe = `id: tavern_discovery
1026
1186
  title: "@journal.tavern_discovery.title"
1027
1187
  text: "@journal.tavern_discovery.text"
1028
1188
  category: places
1029
- `, _e = `# ===================
1189
+ `, De = `# ===================
1030
1190
  # Narrator Intros
1031
1191
  # ===================
1032
1192
  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."
@@ -1171,19 +1331,19 @@ bluff.failure.bartender: "Nope. That'll be five gold."
1171
1331
  bluff.choice.cheers: "Cheers!"
1172
1332
  bluff.choice.fine_pay: "Fine. Here's five gold."
1173
1333
  bluff.choice.walk_away: "Never mind then."
1174
- `, Ee = `id: market
1334
+ `, Te = `id: market
1175
1335
  name: "@location.market.name"
1176
1336
  description: "@location.market.description"
1177
1337
  banner: ""
1178
1338
  music: ""
1179
1339
  ambient: ""
1180
- `, we = `id: tavern
1340
+ `, Ne = `id: tavern
1181
1341
  name: "@location.tavern.name"
1182
1342
  description: "@location.tavern.description"
1183
1343
  banner: ""
1184
1344
  music: ""
1185
1345
  ambient: ""
1186
- `, ke = `id: town
1346
+ `, Ce = `id: town
1187
1347
  name: "@map.town.name"
1188
1348
  image: ""
1189
1349
  scale: 1
@@ -1194,7 +1354,7 @@ locations:
1194
1354
  - id: market
1195
1355
  x: 300
1196
1356
  y: 150
1197
- `, ve = `id: odd_jobs
1357
+ `, Ie = `id: odd_jobs
1198
1358
  name: "@quest.odd_jobs.name"
1199
1359
  description: "@quest.odd_jobs.description"
1200
1360
  stages:
@@ -1204,10 +1364,10 @@ stages:
1204
1364
  description: "@quest.odd_jobs.stage.talked_to_merchant"
1205
1365
  - id: complete
1206
1366
  description: "@quest.odd_jobs.stage.complete"
1207
- `, Oe = `import { useEffect, useState } from 'react'
1367
+ `, Re = `import { useEffect, useState } from 'react'
1208
1368
  import { Engine } from '@doodle-engine/core'
1209
1369
  import type { GameState, Snapshot } from '@doodle-engine/core'
1210
- import { GameProvider, LoadingScreen, useGame } from '@doodle-engine/react'
1370
+ import { GameProvider, useGame } from '@doodle-engine/react'
1211
1371
 
1212
1372
  export function App() {
1213
1373
  const [game, setGame] = useState<{ engine: Engine; snapshot: Snapshot } | null>(null)
@@ -1223,7 +1383,7 @@ export function App() {
1223
1383
  }, [])
1224
1384
 
1225
1385
  if (!game) {
1226
- return <LoadingScreen />
1386
+ return <div className="app-bootstrap"><div className="spinner" /></div>
1227
1387
  }
1228
1388
 
1229
1389
  return (
@@ -1296,36 +1456,49 @@ function createEmptyState(): GameState {
1296
1456
  currentLocale: 'en',
1297
1457
  }
1298
1458
  }
1299
- `, De = `import { useEffect, useState } from 'react'
1300
- import type { ContentRegistry, GameConfig } from '@doodle-engine/core'
1301
- import { GameShell, LoadingScreen } from '@doodle-engine/react'
1459
+ `, Ae = `import { useEffect, useState } from 'react'
1460
+ import type { ContentRegistry, GameConfig, AssetManifest } from '@doodle-engine/core'
1461
+ import { GameShell } from '@doodle-engine/react'
1302
1462
 
1303
1463
  export function App() {
1304
- const [content, setContent] = useState<{ registry: ContentRegistry; config: GameConfig } | null>(null)
1464
+ const [content, setContent] = useState<{
1465
+ registry: ContentRegistry
1466
+ config: GameConfig
1467
+ } | null>(null)
1468
+ const [manifest, setManifest] = useState<AssetManifest | null>(null)
1305
1469
 
1306
1470
  useEffect(() => {
1307
- fetch('/api/content')
1308
- .then(res => res.json())
1309
- .then(data => setContent({ registry: data.registry, config: data.config }))
1471
+ Promise.all([
1472
+ fetch('/api/content').then(res => res.json()),
1473
+ fetch('/api/manifest').then(res => res.json()),
1474
+ ]).then(([contentData, manifestData]) => {
1475
+ setContent({ registry: contentData.registry, config: contentData.config })
1476
+ setManifest(manifestData)
1477
+ })
1310
1478
  }, [])
1311
1479
 
1312
- if (!content) {
1313
- return <LoadingScreen />
1480
+ // Minimal bootstrap state while fetching manifest/content
1481
+ if (!content || !manifest) {
1482
+ return (
1483
+ <div className="app-bootstrap">
1484
+ <div className="spinner" />
1485
+ </div>
1486
+ )
1314
1487
  }
1315
1488
 
1316
1489
  return (
1317
1490
  <GameShell
1318
1491
  registry={content.registry}
1319
1492
  config={content.config}
1493
+ manifest={manifest}
1320
1494
  title="My Doodle Game"
1321
1495
  subtitle="A text-based adventure"
1322
- splashDuration={2000}
1323
1496
  availableLocales={[{ code: 'en', label: 'English' }]}
1324
1497
  devTools={import.meta.env.DEV}
1325
1498
  />
1326
1499
  )
1327
1500
  }
1328
- `, Te = `body {
1501
+ `, Se = `body {
1329
1502
  margin: 0;
1330
1503
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
1331
1504
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -1333,66 +1506,86 @@ export function App() {
1333
1506
  -webkit-font-smoothing: antialiased;
1334
1507
  -moz-osx-font-smoothing: grayscale;
1335
1508
  }
1336
- `, Ne = `import { StrictMode } from 'react'
1509
+
1510
+ /* ── Theme overrides ──────────────────────────────────────────────── */
1511
+ /* Override these variables to customize the shell screens. */
1512
+ /* Full variable list: node_modules/@doodle-engine/react/dist/shell.css */
1513
+
1514
+ /*
1515
+ :root {
1516
+ --doodle-bg-primary: #0f0f23;
1517
+ --doodle-bg-secondary: #1a1a2e;
1518
+ --doodle-accent: #6366f1;
1519
+ }
1520
+ */
1521
+ `, je = `import { StrictMode } from 'react'
1337
1522
  import { createRoot } from 'react-dom/client'
1523
+ import '@doodle-engine/react/style.css'
1338
1524
  import { App } from './App'
1339
1525
  import './index.css'
1340
1526
 
1527
+ // Register service worker in production for offline asset caching
1528
+ if ('serviceWorker' in navigator && import.meta.env.PROD) {
1529
+ navigator.serviceWorker.register('/sw.js').catch(() => {
1530
+ // SW registration failure is non-fatal
1531
+ })
1532
+ }
1533
+
1341
1534
  createRoot(document.getElementById('root')!).render(
1342
1535
  <StrictMode>
1343
1536
  <App />
1344
1537
  </StrictMode>,
1345
1538
  )
1346
- `, O = "ðŸū", Ie = "🐕", j = "ðŸĶī", Ce = "âœĻ", G = "📁", T = "✅", Re = "🚀", A = /* @__PURE__ */ Object.assign({
1347
- "./templates/_root/_gitignore": oe,
1348
- "./templates/_root/index.html": ae,
1349
- "./templates/_root/tsconfig.json": re,
1350
- "./templates/content/characters/bartender.yaml": ie,
1351
- "./templates/content/characters/merchant.yaml": se,
1352
- "./templates/content/dialogues/bartender_greeting.dlg": ce,
1353
- "./templates/content/dialogues/bluff_check.dlg": le,
1354
- "./templates/content/dialogues/market_intro.dlg": de,
1355
- "./templates/content/dialogues/merchant_intro.dlg": ue,
1356
- "./templates/content/dialogues/tavern_intro.dlg": he,
1357
- "./templates/content/game.yaml": me,
1358
- "./templates/content/interludes/chapter_one.yaml": ge,
1359
- "./templates/content/items/old_coin.yaml": fe,
1360
- "./templates/content/journal/market_square.yaml": pe,
1361
- "./templates/content/journal/odd_jobs_accepted.yaml": be,
1362
- "./templates/content/journal/tavern_discovery.yaml": ye,
1363
- "./templates/content/locales/en.yaml": _e,
1364
- "./templates/content/locations/market.yaml": Ee,
1365
- "./templates/content/locations/tavern.yaml": we,
1366
- "./templates/content/maps/town.yaml": ke,
1367
- "./templates/content/quests/odd_jobs.yaml": ve,
1368
- "./templates/src/App.custom.tsx": Oe,
1369
- "./templates/src/App.default.tsx": De,
1370
- "./templates/src/index.css": Te,
1371
- "./templates/src/main.tsx": Ne
1539
+ `, D = "ðŸū", xe = "🐕", H = "ðŸĶī", Ge = "âœĻ", $ = "📁", N = "✅", qe = "🚀", x = /* @__PURE__ */ Object.assign({
1540
+ "./templates/_root/_gitignore": le,
1541
+ "./templates/_root/index.html": de,
1542
+ "./templates/_root/tsconfig.json": ue,
1543
+ "./templates/content/characters/bartender.yaml": he,
1544
+ "./templates/content/characters/merchant.yaml": me,
1545
+ "./templates/content/dialogues/bartender_greeting.dlg": ge,
1546
+ "./templates/content/dialogues/bluff_check.dlg": fe,
1547
+ "./templates/content/dialogues/market_intro.dlg": pe,
1548
+ "./templates/content/dialogues/merchant_intro.dlg": be,
1549
+ "./templates/content/dialogues/tavern_intro.dlg": ye,
1550
+ "./templates/content/game.yaml": _e,
1551
+ "./templates/content/interludes/chapter_one.yaml": we,
1552
+ "./templates/content/items/old_coin.yaml": Ee,
1553
+ "./templates/content/journal/market_square.yaml": ve,
1554
+ "./templates/content/journal/odd_jobs_accepted.yaml": ke,
1555
+ "./templates/content/journal/tavern_discovery.yaml": Oe,
1556
+ "./templates/content/locales/en.yaml": De,
1557
+ "./templates/content/locations/market.yaml": Te,
1558
+ "./templates/content/locations/tavern.yaml": Ne,
1559
+ "./templates/content/maps/town.yaml": Ce,
1560
+ "./templates/content/quests/odd_jobs.yaml": Ie,
1561
+ "./templates/src/App.custom.tsx": Re,
1562
+ "./templates/src/App.default.tsx": Ae,
1563
+ "./templates/src/index.css": Se,
1564
+ "./templates/src/main.tsx": je
1372
1565
  });
1373
- function Ae(n) {
1374
- const a = n.replace("./templates/", "");
1375
- if (a === "src/App.default.tsx" || a === "src/App.custom.tsx") return null;
1376
- if (a.startsWith("_root/")) {
1377
- const r = a.slice(6);
1378
- return r.startsWith("_") ? "." + r.slice(1) : r;
1566
+ function He(t) {
1567
+ const o = t.replace("./templates/", "");
1568
+ if (o === "src/App.default.tsx" || o === "src/App.custom.tsx") return null;
1569
+ if (o.startsWith("_root/")) {
1570
+ const s = o.slice(6);
1571
+ return s.startsWith("_") ? "." + s.slice(1) : s;
1379
1572
  }
1380
- return a;
1573
+ return o;
1381
1574
  }
1382
- async function Se(n) {
1383
- const a = h(process.cwd(), n);
1384
- console.log(""), console.log(s.bold.magenta(` ${O} Doodle Engine ${O}`)), console.log(s.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${Ie} Creating new game: ${s.bold.cyan(n)}`), console.log("");
1385
- const { useDefaultRenderer: r } = await F({
1575
+ async function $e(t) {
1576
+ const o = h(process.cwd(), t);
1577
+ console.log(""), console.log(c.bold.magenta(` ${D} Doodle Engine ${D}`)), console.log(c.dim(" Text-based RPG and Adventure Game Scaffolder")), console.log(""), console.log(` ${xe} Creating new game: ${c.bold.cyan(t)}`), console.log("");
1578
+ const { useDefaultRenderer: s } = await J({
1386
1579
  type: "confirm",
1387
1580
  name: "useDefaultRenderer",
1388
1581
  message: "Use default renderer?",
1389
1582
  initial: !0
1390
1583
  });
1391
- r === void 0 && (console.log(s.yellow(`
1392
- ${j} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await je(a, n, r), console.log(""), console.log(s.bold.green(` ${T} Project created successfully!`)), console.log(""), console.log(s.dim(` ${G} ${a}`)), console.log(""), console.log(s.bold(" Next steps:")), console.log(s.cyan(` cd ${n}`)), console.log(s.cyan(" npm install ") + s.dim("# or: yarn install / pnpm install")), console.log(s.cyan(" npm run dev ") + s.dim("# or: yarn dev / pnpm dev")), console.log(""), console.log(s.dim(` ${Re} Happy game making! ${O}`)), console.log("");
1584
+ s === void 0 && (console.log(c.yellow(`
1585
+ ${H} No worries, maybe next time! Woof!`)), process.exit(0)), console.log(""), await Me(o, t, s), console.log(""), console.log(c.bold.green(` ${N} Project created successfully!`)), console.log(""), console.log(c.dim(` ${$} ${o}`)), console.log(""), console.log(c.bold(" Next steps:")), console.log(c.cyan(` cd ${t}`)), console.log(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(` ${qe} Happy game making! ${D}`)), console.log("");
1393
1586
  }
1394
- async function je(n, a, r) {
1395
- const o = [
1587
+ async function Me(t, o, s) {
1588
+ const r = [
1396
1589
  "content/locations",
1397
1590
  "content/characters",
1398
1591
  "content/items",
@@ -1411,12 +1604,12 @@ async function je(n, a, r) {
1411
1604
  "assets/audio/voice",
1412
1605
  "src"
1413
1606
  ];
1414
- console.log(` ${G} ${s.bold("Creating directories...")}`);
1415
- for (const e of o)
1416
- await D(h(n, e), { recursive: !0 });
1417
- console.log(s.green(` ${T} Directories created`)), console.log("");
1418
- const t = {
1419
- name: a,
1607
+ console.log(` ${$} ${c.bold("Creating directories...")}`);
1608
+ for (const e of r)
1609
+ await T(h(t, e), { recursive: !0 });
1610
+ console.log(c.green(` ${N} Directories created`)), console.log("");
1611
+ const n = {
1612
+ name: o,
1420
1613
  version: "0.1.0",
1421
1614
  type: "module",
1422
1615
  scripts: {
@@ -1439,28 +1632,28 @@ async function je(n, a, r) {
1439
1632
  vite: "^6.0.0"
1440
1633
  }
1441
1634
  };
1442
- console.log(` ${Ce} ${s.bold("Writing project files...")}`), await w(h(n, "package.json"), JSON.stringify(t, null, 2));
1443
- for (const [e, c] of Object.entries(A)) {
1444
- const l = Ae(e);
1635
+ console.log(` ${Ge} ${c.bold("Writing project files...")}`), await w(h(t, "package.json"), JSON.stringify(n, null, 2));
1636
+ for (const [e, i] of Object.entries(x)) {
1637
+ const l = He(e);
1445
1638
  if (l === null) continue;
1446
- const d = h(n, l);
1447
- await D(M(d), { recursive: !0 }), await w(d, c);
1639
+ const d = h(t, l);
1640
+ await T(U(d), { recursive: !0 }), await w(d, i);
1448
1641
  }
1449
- const i = r ? "./templates/src/App.default.tsx" : "./templates/src/App.custom.tsx";
1450
- await w(h(n, "src/App.tsx"), A[i]), console.log(s.green(` ${T} Source files created`)), console.log(""), console.log(` ${j} ${s.bold("Starter content written")}`), console.log(""), console.log(s.dim(" Content includes:")), console.log(s.dim(" 2 locations (tavern, market)")), console.log(s.dim(" 2 characters (bartender, merchant)")), console.log(s.dim(" 1 item (old coin)")), console.log(s.dim(" 1 map (town with 2 locations)")), console.log(s.dim(" 1 quest (odd jobs, 3 stages)")), console.log(s.dim(" 3 journal entries")), console.log(s.dim(" 1 interlude (chapter one, auto-triggers at tavern)")), console.log(s.dim(" 5 dialogues (2 narrator intros, 2 NPC conversations, 1 skill check)")), console.log(s.dim(" English locale with all strings"));
1642
+ const a = s ? "./templates/src/App.default.tsx" : "./templates/src/App.custom.tsx";
1643
+ await w(h(t, "src/App.tsx"), x[a]), console.log(c.green(` ${N} Source files created`)), console.log(""), console.log(` ${H} ${c.bold("Starter content written")}`), console.log(""), console.log(c.dim(" Content includes:")), console.log(c.dim(" 2 locations (tavern, market)")), console.log(c.dim(" 2 characters (bartender, merchant)")), console.log(c.dim(" 1 item (old coin)")), console.log(c.dim(" 1 map (town with 2 locations)")), console.log(c.dim(" 1 quest (odd jobs, 3 stages)")), console.log(c.dim(" 3 journal entries")), console.log(c.dim(" 1 interlude (chapter one, auto-triggers at tavern)")), console.log(c.dim(" 5 dialogues (2 narrator intros, 2 NPC conversations, 1 skill check)")), console.log(c.dim(" English locale with all strings"));
1451
1644
  }
1452
- const _ = new x();
1453
- _.name("doodle").description(s.magenta("ðŸū Doodle Engine") + s.dim(" — Narrative RPG development tools")).version("0.0.1");
1454
- _.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (n) => {
1455
- await Se(n);
1645
+ const v = new M();
1646
+ v.name("doodle").description(c.magenta("ðŸū Doodle Engine") + c.dim(" — Narrative RPG development tools")).version("0.0.1");
1647
+ v.command("create <project-name>").description("Scaffold a new Doodle Engine game project").action(async (t) => {
1648
+ await $e(t);
1456
1649
  });
1457
- _.command("dev").description("Start development server with hot reload").action(async () => {
1458
- await V();
1650
+ v.command("dev").description("Start development server with hot reload").action(async () => {
1651
+ await te();
1459
1652
  });
1460
- _.command("build").description("Build game for production").action(async () => {
1461
- await Z();
1653
+ v.command("build").description("Build game for production").action(async () => {
1654
+ await re();
1462
1655
  });
1463
- _.command("validate").description("Validate game content").action(async () => {
1464
- await ne();
1656
+ v.command("validate").description("Validate game content").action(async () => {
1657
+ await ie();
1465
1658
  });
1466
- _.parse();
1659
+ v.parse();