@runek/core 0.6.0 → 0.10.0

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +245 -28
  2. package/dist/index.js +706 -83
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  // src/context.ts
2
2
  import { createContext } from "react";
3
3
 
4
+ // src/font.ts
5
+ var DEFAULT_FONT = "data:font/woff2;base64,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";
6
+ var DEFAULT_FONTS = {
7
+ display: DEFAULT_FONT,
8
+ body: DEFAULT_FONT
9
+ };
10
+
4
11
  // src/palette.ts
5
12
  var DEFAULT_PALETTE = {
6
13
  wood: "#6b4f3a",
@@ -20,13 +27,101 @@ var DEFAULT_PALETTE = {
20
27
  waterShallow: "#3f86a8"
21
28
  };
22
29
 
30
+ // src/time.ts
31
+ var DEFAULT_WORLD_TIME = { hours: 12, live: false };
32
+ function parseClockTime(value) {
33
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
34
+ if (!match) return null;
35
+ const h = Number(match[1]);
36
+ const m = Number(match[2]);
37
+ if (h < 0 || h > 23 || m < 0 || m > 59) return null;
38
+ return h + m / 60;
39
+ }
40
+ function clockHours(timezone) {
41
+ const now = /* @__PURE__ */ new Date();
42
+ if (timezone) {
43
+ try {
44
+ const parts = new Intl.DateTimeFormat("en-GB", {
45
+ timeZone: timezone,
46
+ hour: "2-digit",
47
+ minute: "2-digit",
48
+ hour12: false
49
+ }).formatToParts(now);
50
+ const get = (type) => Number(parts.find((p) => p.type === type)?.value);
51
+ const h2 = get("hour") % 24;
52
+ const m2 = get("minute");
53
+ if (Number.isFinite(h2) && Number.isFinite(m2)) return h2 + m2 / 60;
54
+ } catch {
55
+ }
56
+ return now.getUTCHours() + now.getUTCMinutes() / 60;
57
+ }
58
+ const h = now.getHours();
59
+ const m = now.getMinutes();
60
+ if (Number.isFinite(h) && Number.isFinite(m)) return h + m / 60;
61
+ return now.getUTCHours() + now.getUTCMinutes() / 60;
62
+ }
63
+ function resolveWorldTime(opts) {
64
+ if (opts.time !== void 0) {
65
+ const hours = parseClockTime(opts.time);
66
+ if (hours !== null) return { hours, live: false, timezone: opts.timezone };
67
+ }
68
+ return { hours: clockHours(opts.timezone), live: true, timezone: opts.timezone };
69
+ }
70
+ function currentHours(time) {
71
+ return time.live ? clockHours(time.timezone) : time.hours;
72
+ }
73
+ function sunState(hours, radius = 100) {
74
+ const theta = (hours - 6) / 12 * Math.PI;
75
+ const sinEl = Math.sin(theta);
76
+ const elevation = Math.max(0, sinEl);
77
+ const x = Math.cos(theta);
78
+ const y = sinEl;
79
+ const z = 0.3;
80
+ const len = Math.hypot(x, y, z) || 1;
81
+ return {
82
+ position: [x / len * radius, y / len * radius, z / len * radius],
83
+ elevation,
84
+ day: sinEl > 0
85
+ };
86
+ }
87
+
23
88
  // src/context.ts
24
89
  var WorldContext = createContext({
25
90
  unit: 1,
26
91
  gravity: [0, -9.81, 0],
27
- palette: DEFAULT_PALETTE
92
+ ground: 0,
93
+ palette: DEFAULT_PALETTE,
94
+ fonts: DEFAULT_FONTS,
95
+ time: DEFAULT_WORLD_TIME
28
96
  });
29
97
 
98
+ // src/contribute.ts
99
+ function parseGitHubSource(source) {
100
+ if (!source?.url) return null;
101
+ let url;
102
+ try {
103
+ url = new URL(source.url);
104
+ } catch {
105
+ return null;
106
+ }
107
+ if (url.hostname !== "github.com" && url.hostname !== "www.github.com") return null;
108
+ const parts = url.pathname.split("/").filter(Boolean);
109
+ if (parts.length < 2) return null;
110
+ return {
111
+ owner: parts[0],
112
+ repo: parts[1].replace(/\.git$/, ""),
113
+ branch: source.branch?.trim() || "main",
114
+ path: source.path?.trim() || void 0
115
+ };
116
+ }
117
+ function forkUrl(gh) {
118
+ return `https://github.com/${gh.owner}/${gh.repo}/fork`;
119
+ }
120
+ function editFileUrl(gh) {
121
+ if (!gh.path) return null;
122
+ return `https://github.com/${gh.owner}/${gh.repo}/edit/${gh.branch}/${gh.path}`;
123
+ }
124
+
30
125
  // src/keyboard.ts
31
126
  var keyboardMap = [
32
127
  { name: "forward", keys: ["ArrowUp", "KeyW"] },
@@ -64,50 +159,240 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
64
159
  function World({
65
160
  unit = 1,
66
161
  gravity = [0, -9.81, 0],
162
+ ground = 0,
67
163
  keyboardMap: keyboardMap2 = keyboardMap,
68
164
  lights = true,
69
165
  palette,
166
+ fonts,
70
167
  fog,
168
+ time,
169
+ timezone,
170
+ avatar,
71
171
  onPointerMissed,
172
+ preserveDrawingBuffer = false,
72
173
  debug = false,
73
174
  children
74
175
  }) {
75
176
  const context = useMemo(
76
- () => ({ unit, gravity, palette: { ...DEFAULT_PALETTE, ...palette } }),
77
- [unit, gravity, palette]
177
+ () => ({
178
+ unit,
179
+ gravity,
180
+ ground,
181
+ palette: { ...DEFAULT_PALETTE, ...palette },
182
+ fonts: { ...DEFAULT_FONTS, ...fonts },
183
+ time: resolveWorldTime({ time, timezone }),
184
+ avatar
185
+ }),
186
+ [unit, gravity, ground, palette, fonts, time, timezone, avatar]
78
187
  );
79
- return /* @__PURE__ */ jsx(KeyboardControls, { map: keyboardMap2, children: /* @__PURE__ */ jsx(Canvas, { shadows: true, camera: { position: [6, 4, 6], fov: 60 }, onPointerMissed, children: /* @__PURE__ */ jsxs(WorldContext.Provider, { value: context, children: [
80
- fog && /* @__PURE__ */ jsx("fog", { attach: "fog", args: [fog.color, fog.near * unit, fog.far * unit] }),
81
- lights && /* @__PURE__ */ jsxs(Fragment, { children: [
82
- /* @__PURE__ */ jsx("ambientLight", { intensity: 0.6 }),
83
- /* @__PURE__ */ jsx(
84
- "directionalLight",
85
- {
86
- position: [12, 18, 8],
87
- intensity: 1.6,
88
- castShadow: true,
89
- "shadow-mapSize": [2048, 2048],
90
- "shadow-camera-near": 1,
91
- "shadow-camera-far": 60,
92
- "shadow-camera-left": -25,
93
- "shadow-camera-right": 25,
94
- "shadow-camera-top": 25,
95
- "shadow-camera-bottom": -25
96
- }
97
- )
98
- ] }),
99
- /* @__PURE__ */ jsx(Physics, { gravity, debug, children })
100
- ] }) }) });
188
+ return /* @__PURE__ */ jsx(KeyboardControls, { map: keyboardMap2, children: /* @__PURE__ */ jsx(
189
+ Canvas,
190
+ {
191
+ shadows: true,
192
+ camera: { position: [6, 4, 6], fov: 60 },
193
+ gl: { preserveDrawingBuffer },
194
+ onPointerMissed,
195
+ children: /* @__PURE__ */ jsxs(WorldContext.Provider, { value: context, children: [
196
+ fog && /* @__PURE__ */ jsx("fog", { attach: "fog", args: [fog.color, fog.near * unit, fog.far * unit] }),
197
+ lights && /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx("ambientLight", { intensity: 0.6 }),
199
+ /* @__PURE__ */ jsx(
200
+ "directionalLight",
201
+ {
202
+ position: [12, 18, 8],
203
+ intensity: 1.6,
204
+ castShadow: true,
205
+ "shadow-mapSize": [2048, 2048],
206
+ "shadow-camera-near": 1,
207
+ "shadow-camera-far": 60,
208
+ "shadow-camera-left": -25,
209
+ "shadow-camera-right": 25,
210
+ "shadow-camera-top": 25,
211
+ "shadow-camera-bottom": -25
212
+ }
213
+ )
214
+ ] }),
215
+ /* @__PURE__ */ jsx(Physics, { gravity, debug, children })
216
+ ] })
217
+ }
218
+ ) });
101
219
  }
102
220
 
103
- // src/WorldEditor.tsx
104
- import { OrbitControls, TransformControls } from "@react-three/drei";
105
- import { Leva, useControls } from "leva";
106
- import { useEffect, useRef, useState } from "react";
221
+ // src/WorldAbout.tsx
222
+ import { useEffect, useState } from "react";
223
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
224
+ function WorldAbout({ meta }) {
225
+ const [open, setOpen] = useState(false);
226
+ useEffect(() => {
227
+ if (!open) return;
228
+ const onKey = (event) => {
229
+ if (event.key === "Escape") setOpen(false);
230
+ };
231
+ window.addEventListener("keydown", onKey);
232
+ return () => window.removeEventListener("keydown", onKey);
233
+ }, [open]);
234
+ const title = meta?.title?.trim() || "Untitled world";
235
+ const authors = meta?.authors ?? [];
236
+ const source = meta?.source;
237
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
238
+ /* @__PURE__ */ jsx2(
239
+ "button",
240
+ {
241
+ type: "button",
242
+ style: INFO_BTN,
243
+ onClick: () => setOpen(true),
244
+ title: "About this world",
245
+ "aria-label": "About this world",
246
+ children: "\u24D8"
247
+ }
248
+ ),
249
+ open && /* @__PURE__ */ jsxs2("div", { style: OVERLAY, role: "dialog", "aria-modal": "true", "aria-label": "About this world", children: [
250
+ /* @__PURE__ */ jsx2("button", { type: "button", style: SCRIM, "aria-label": "Close", onClick: () => setOpen(false) }),
251
+ /* @__PURE__ */ jsxs2("div", { style: CARD, children: [
252
+ /* @__PURE__ */ jsx2("button", { type: "button", style: CLOSE, onClick: () => setOpen(false), "aria-label": "Close", children: "\xD7" }),
253
+ /* @__PURE__ */ jsx2("h2", { style: TITLE, children: title }),
254
+ meta?.description && /* @__PURE__ */ jsx2("p", { style: DESC, children: meta.description }),
255
+ authors.length > 0 && /* @__PURE__ */ jsxs2("p", { style: ROW, children: [
256
+ /* @__PURE__ */ jsx2("span", { style: LABEL, children: "by " }),
257
+ authors.map((author, index) => /* @__PURE__ */ jsxs2("span", { children: [
258
+ index > 0 && ", ",
259
+ author.url ? /* @__PURE__ */ jsx2("a", { style: LINK, href: author.url, target: "_blank", rel: "noreferrer", children: author.name }) : author.name
260
+ ] }, `${author.name}-${index}`))
261
+ ] }),
262
+ meta?.license && /* @__PURE__ */ jsxs2("p", { style: ROW, children: [
263
+ /* @__PURE__ */ jsx2("span", { style: LABEL, children: "license " }),
264
+ meta.license
265
+ ] }),
266
+ source?.url && /* @__PURE__ */ jsx2("p", { style: ROW, children: /* @__PURE__ */ jsx2("a", { style: LINK, href: source.url, target: "_blank", rel: "noreferrer", children: "View source repository \u2197" }) }),
267
+ /* @__PURE__ */ jsx2("p", { style: BUILT, children: /* @__PURE__ */ jsx2(
268
+ "a",
269
+ {
270
+ style: BUILT_LINK,
271
+ href: "https://runek.nullorder.org",
272
+ target: "_blank",
273
+ rel: "noreferrer",
274
+ children: "Built with Runek"
275
+ }
276
+ ) })
277
+ ] })
278
+ ] })
279
+ ] });
280
+ }
281
+ var MONO = "ui-monospace, SF Mono, Menlo, monospace";
282
+ var PANEL_BG = "rgba(7, 11, 17, 0.95)";
283
+ var BORDER = "#15202a";
284
+ var INFO_BTN = {
285
+ position: "fixed",
286
+ top: "1rem",
287
+ right: "1rem",
288
+ zIndex: 10,
289
+ width: 32,
290
+ height: 32,
291
+ borderRadius: "50%",
292
+ border: `1px solid ${BORDER}`,
293
+ background: "rgba(7, 11, 17, 0.82)",
294
+ color: "#cfe6db",
295
+ backdropFilter: "blur(8px)",
296
+ cursor: "pointer",
297
+ fontSize: "0.95rem",
298
+ lineHeight: 1,
299
+ fontFamily: MONO
300
+ };
301
+ var OVERLAY = {
302
+ position: "fixed",
303
+ inset: 0,
304
+ zIndex: 20,
305
+ display: "flex",
306
+ alignItems: "center",
307
+ justifyContent: "center",
308
+ padding: "1rem"
309
+ };
310
+ var SCRIM = {
311
+ position: "absolute",
312
+ inset: 0,
313
+ border: "none",
314
+ padding: 0,
315
+ margin: 0,
316
+ background: "rgba(3, 5, 10, 0.6)",
317
+ backdropFilter: "blur(2px)",
318
+ cursor: "default"
319
+ };
320
+ var CARD = {
321
+ position: "relative",
322
+ zIndex: 1,
323
+ width: "100%",
324
+ maxWidth: 420,
325
+ padding: "1.5rem 1.6rem",
326
+ borderRadius: 12,
327
+ background: PANEL_BG,
328
+ border: `1px solid ${BORDER}`,
329
+ color: "#cfe6db",
330
+ fontFamily: MONO,
331
+ boxShadow: "0 20px 60px rgba(0, 0, 0, 0.5)"
332
+ };
333
+ var CLOSE = {
334
+ position: "absolute",
335
+ top: "0.7rem",
336
+ right: "0.8rem",
337
+ border: "none",
338
+ background: "transparent",
339
+ color: "#5f7d75",
340
+ cursor: "pointer",
341
+ fontSize: "1.1rem",
342
+ lineHeight: 1,
343
+ fontFamily: MONO
344
+ };
345
+ var TITLE = {
346
+ margin: "0 0 0.5rem",
347
+ fontSize: "1.15rem",
348
+ fontWeight: 600,
349
+ color: "#3df58a"
350
+ };
351
+ var DESC = {
352
+ margin: "0 0 0.9rem",
353
+ fontSize: "0.85rem",
354
+ lineHeight: 1.5
355
+ };
356
+ var ROW = {
357
+ margin: "0 0 0.4rem",
358
+ fontSize: "0.8rem"
359
+ };
360
+ var LABEL = { color: "#5f7d75" };
361
+ var LINK = { color: "#2aa7ff", textDecoration: "none" };
362
+ var BUILT = {
363
+ margin: "1.1rem 0 0",
364
+ paddingTop: "0.8rem",
365
+ borderTop: `1px solid ${BORDER}`,
366
+ fontSize: "0.72rem",
367
+ color: "#5f7d75"
368
+ };
369
+ var BUILT_LINK = { color: "#5f7d75", textDecoration: "none" };
370
+
371
+ // src/WorldContribute.tsx
372
+ import { useEffect as useEffect2 } from "react";
107
373
 
108
374
  // src/world-data.ts
375
+ function normalizeNode(node) {
376
+ const out = { type: node.type };
377
+ if (node.id !== void 0) out.id = node.id;
378
+ if (node.props !== void 0) out.props = node.props;
379
+ if (node.children !== void 0) out.children = node.children.map(normalizeNode);
380
+ return out;
381
+ }
109
382
  function serializeWorld(data) {
110
- return `${JSON.stringify(data, null, 2)}
383
+ const out = { version: data.version };
384
+ if (data.meta !== void 0) out.meta = data.meta;
385
+ if (data.unit !== void 0) out.unit = data.unit;
386
+ if (data.gravity !== void 0) out.gravity = data.gravity;
387
+ if (data.ground !== void 0) out.ground = data.ground;
388
+ if (data.time !== void 0) out.time = data.time;
389
+ if (data.timezone !== void 0) out.timezone = data.timezone;
390
+ if (data.avatar !== void 0) out.avatar = data.avatar;
391
+ if (data.palette !== void 0) out.palette = data.palette;
392
+ if (data.fonts !== void 0) out.fonts = data.fonts;
393
+ if (data.fog !== void 0) out.fog = data.fog;
394
+ out.nodes = data.nodes.map(normalizeNode);
395
+ return `${JSON.stringify(out, null, 2)}
111
396
  `;
112
397
  }
113
398
  function parseWorld(json) {
@@ -120,23 +405,316 @@ function parseWorld(json) {
120
405
  if (!Array.isArray(data.nodes)) {
121
406
  throw new Error('World data must have a "nodes" array');
122
407
  }
408
+ if (data.meta !== void 0) {
409
+ const meta = data.meta;
410
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) {
411
+ throw new Error('World "meta" must be an object');
412
+ }
413
+ if (meta.authors !== void 0 && !Array.isArray(meta.authors)) {
414
+ throw new Error('World "meta.authors" must be an array');
415
+ }
416
+ }
417
+ if (data.time !== void 0 && typeof data.time !== "string") {
418
+ throw new Error('World "time" must be an "HH:MM" string');
419
+ }
420
+ if (data.timezone !== void 0 && typeof data.timezone !== "string") {
421
+ throw new Error('World "timezone" must be a string');
422
+ }
423
+ if (data.avatar !== void 0 && data.avatar !== "first" && data.avatar !== "third") {
424
+ throw new Error('World "avatar" must be "first" or "third"');
425
+ }
426
+ if (data.ground !== void 0 && typeof data.ground !== "number") {
427
+ throw new Error('World "ground" must be a number');
428
+ }
429
+ if (data.fonts !== void 0) {
430
+ const fonts = data.fonts;
431
+ if (typeof fonts !== "object" || fonts === null || Array.isArray(fonts)) {
432
+ throw new Error('World "fonts" must be an object of role to font URL');
433
+ }
434
+ for (const value of Object.values(fonts)) {
435
+ if (typeof value !== "string") {
436
+ throw new Error('World "fonts" values must be font URL strings');
437
+ }
438
+ }
439
+ }
123
440
  return data;
124
441
  }
442
+ function collectIds(nodes, into) {
443
+ for (const node of nodes) {
444
+ if (node.id) into.add(node.id);
445
+ if (node.children) collectIds(node.children, into);
446
+ }
447
+ }
448
+ function makeNodeId(taken) {
449
+ let id;
450
+ do {
451
+ id = `n${Math.random().toString(36).slice(2, 8)}`;
452
+ } while (taken.has(id));
453
+ taken.add(id);
454
+ return id;
455
+ }
456
+ function withIds(nodes, taken) {
457
+ return nodes.map((node) => ({
458
+ ...node,
459
+ id: node.id ?? makeNodeId(taken),
460
+ ...node.children ? { children: withIds(node.children, taken) } : {}
461
+ }));
462
+ }
463
+ function assignNodeIds(data) {
464
+ const taken = /* @__PURE__ */ new Set();
465
+ collectIds(data.nodes, taken);
466
+ return { ...data, nodes: withIds(data.nodes, taken) };
467
+ }
468
+
469
+ // src/WorldContribute.tsx
470
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
471
+ function downloadHref(href, filename) {
472
+ const a = document.createElement("a");
473
+ a.href = href;
474
+ a.download = filename;
475
+ a.click();
476
+ }
477
+ function downloadText(text, filename) {
478
+ const url = URL.createObjectURL(new Blob([text], { type: "application/json" }));
479
+ downloadHref(url, filename);
480
+ URL.revokeObjectURL(url);
481
+ }
482
+ function captureSnapshot() {
483
+ const canvas = document.querySelector("canvas");
484
+ if (!canvas) return null;
485
+ try {
486
+ return canvas.toDataURL("image/png");
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
491
+ function WorldContribute({ data, onClose }) {
492
+ const source = data.meta?.source;
493
+ const gh = parseGitHubSource(source);
494
+ const editUrl = gh ? editFileUrl(gh) : null;
495
+ useEffect2(() => {
496
+ const onKey = (event) => {
497
+ if (event.key === "Escape") onClose();
498
+ };
499
+ window.addEventListener("keydown", onKey);
500
+ return () => window.removeEventListener("keydown", onKey);
501
+ }, [onClose]);
502
+ const downloadJson = () => downloadText(serializeWorld(data), "world.json");
503
+ const downloadSnapshot = () => {
504
+ const png = captureSnapshot();
505
+ if (png) downloadHref(png, "world-snapshot.png");
506
+ };
507
+ const open = (url) => window.open(url, "_blank", "noopener,noreferrer");
508
+ return /* @__PURE__ */ jsxs3("div", { style: OVERLAY2, role: "dialog", "aria-modal": "true", "aria-label": "Contribute this world", children: [
509
+ /* @__PURE__ */ jsx3("button", { type: "button", style: SCRIM2, "aria-label": "Close", onClick: onClose }),
510
+ /* @__PURE__ */ jsxs3("div", { style: CARD2, children: [
511
+ /* @__PURE__ */ jsx3("button", { type: "button", style: CLOSE2, onClick: onClose, "aria-label": "Close", children: "\xD7" }),
512
+ /* @__PURE__ */ jsx3("h2", { style: TITLE2, children: "Contribute this world" }),
513
+ /* @__PURE__ */ jsx3("p", { style: INTRO, children: "Runek hands your edit to GitHub directly, no account or token needed. Your change becomes a normal pull request the world's owner reviews." }),
514
+ gh ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
515
+ /* @__PURE__ */ jsx3("button", { type: "button", style: SECONDARY, onClick: () => open(forkUrl(gh)), children: "Fork this world \u2197" }),
516
+ /* @__PURE__ */ jsx3("p", { style: HINT, children: "Get your own deployable copy to build on." }),
517
+ /* @__PURE__ */ jsx3("div", { style: DIVIDER }),
518
+ /* @__PURE__ */ jsxs3("ol", { style: STEPS, children: [
519
+ /* @__PURE__ */ jsxs3(Step, { n: 1, children: [
520
+ "Download your edits and a snapshot of this view.",
521
+ /* @__PURE__ */ jsxs3("div", { style: ACTIONS, children: [
522
+ /* @__PURE__ */ jsx3("button", { type: "button", style: PRIMARY, onClick: downloadJson, children: "Download world.json" }),
523
+ /* @__PURE__ */ jsx3("button", { type: "button", style: PRIMARY, onClick: downloadSnapshot, children: "Download snapshot.png" })
524
+ ] })
525
+ ] }),
526
+ /* @__PURE__ */ jsxs3(Step, { n: 2, children: [
527
+ "Open the world file on GitHub (it forks the repo for you automatically).",
528
+ /* @__PURE__ */ jsx3("div", { style: ACTIONS, children: editUrl ? /* @__PURE__ */ jsx3("button", { type: "button", style: PRIMARY, onClick: () => open(editUrl), children: "Open GitHub editor \u2192" }) : /* @__PURE__ */ jsx3(
529
+ "button",
530
+ {
531
+ type: "button",
532
+ style: PRIMARY,
533
+ onClick: () => source && open(source.url),
534
+ children: "Open repository \u2197"
535
+ }
536
+ ) }),
537
+ !editUrl && /* @__PURE__ */ jsxs3("span", { style: NOTE, children: [
538
+ "The world's file path isn't set in ",
539
+ /* @__PURE__ */ jsx3("code", { children: "meta.source.path" }),
540
+ ", so open the repo and edit the world file directly."
541
+ ] })
542
+ ] }),
543
+ /* @__PURE__ */ jsx3(Step, { n: 3, children: "In GitHub's editor: select all, paste the downloaded JSON, and commit to a new branch." }),
544
+ /* @__PURE__ */ jsx3(Step, { n: 4, children: "GitHub offers \u201CPropose changes\u201D \u2192 open the pull request. Attach the snapshot so the owner can see the change a JSON diff can't show." })
545
+ ] })
546
+ ] }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
547
+ /* @__PURE__ */ jsx3("p", { style: INTRO, children: "This world's repository isn't on GitHub. Download your changes and open the repo to contribute however it accepts them." }),
548
+ /* @__PURE__ */ jsxs3("div", { style: ACTIONS, children: [
549
+ /* @__PURE__ */ jsx3("button", { type: "button", style: PRIMARY, onClick: downloadJson, children: "Download world.json" }),
550
+ source?.url && /* @__PURE__ */ jsx3("button", { type: "button", style: PRIMARY, onClick: () => open(source.url), children: "Open repository \u2197" })
551
+ ] })
552
+ ] })
553
+ ] })
554
+ ] });
555
+ }
556
+ function Step({ n, children }) {
557
+ return /* @__PURE__ */ jsxs3("li", { style: STEP, children: [
558
+ /* @__PURE__ */ jsx3("span", { style: STEP_NUM, children: n }),
559
+ /* @__PURE__ */ jsx3("div", { children })
560
+ ] });
561
+ }
562
+ var MONO2 = "ui-monospace, SF Mono, Menlo, monospace";
563
+ var BORDER2 = "#15202a";
564
+ var GREEN = "#3df58a";
565
+ var OVERLAY2 = {
566
+ position: "fixed",
567
+ inset: 0,
568
+ zIndex: 20,
569
+ display: "flex",
570
+ alignItems: "center",
571
+ justifyContent: "center",
572
+ padding: "1rem"
573
+ };
574
+ var SCRIM2 = {
575
+ position: "absolute",
576
+ inset: 0,
577
+ border: "none",
578
+ padding: 0,
579
+ margin: 0,
580
+ background: "rgba(3, 5, 10, 0.6)",
581
+ backdropFilter: "blur(2px)",
582
+ cursor: "default"
583
+ };
584
+ var CARD2 = {
585
+ position: "relative",
586
+ zIndex: 1,
587
+ width: "100%",
588
+ maxWidth: 480,
589
+ maxHeight: "90vh",
590
+ overflowY: "auto",
591
+ padding: "1.5rem 1.6rem",
592
+ borderRadius: 12,
593
+ background: "rgba(7, 11, 17, 0.97)",
594
+ border: `1px solid ${BORDER2}`,
595
+ color: "#cfe6db",
596
+ fontFamily: MONO2,
597
+ boxShadow: "0 20px 60px rgba(0, 0, 0, 0.5)"
598
+ };
599
+ var CLOSE2 = {
600
+ position: "absolute",
601
+ top: "0.7rem",
602
+ right: "0.8rem",
603
+ border: "none",
604
+ background: "transparent",
605
+ color: "#5f7d75",
606
+ cursor: "pointer",
607
+ fontSize: "1.1rem",
608
+ lineHeight: 1,
609
+ fontFamily: MONO2
610
+ };
611
+ var TITLE2 = {
612
+ margin: "0 0 0.5rem",
613
+ fontSize: "1.15rem",
614
+ fontWeight: 600,
615
+ color: GREEN
616
+ };
617
+ var INTRO = {
618
+ margin: "0 0 1rem",
619
+ fontSize: "0.82rem",
620
+ lineHeight: 1.5,
621
+ color: "#cfe6db"
622
+ };
623
+ var HINT = {
624
+ margin: "0.35rem 0 0",
625
+ fontSize: "0.72rem",
626
+ color: "#5f7d75"
627
+ };
628
+ var NOTE = {
629
+ display: "block",
630
+ marginTop: "0.4rem",
631
+ fontSize: "0.72rem",
632
+ color: "#5f7d75"
633
+ };
634
+ var DIVIDER = {
635
+ height: 1,
636
+ margin: "1.1rem 0",
637
+ background: BORDER2
638
+ };
639
+ var STEPS = {
640
+ margin: 0,
641
+ padding: 0,
642
+ listStyle: "none",
643
+ display: "flex",
644
+ flexDirection: "column",
645
+ gap: "0.9rem"
646
+ };
647
+ var STEP = {
648
+ display: "flex",
649
+ gap: "0.6rem",
650
+ fontSize: "0.82rem",
651
+ lineHeight: 1.5
652
+ };
653
+ var STEP_NUM = {
654
+ flex: "0 0 auto",
655
+ width: 20,
656
+ height: 20,
657
+ borderRadius: "50%",
658
+ border: `1px solid ${GREEN}`,
659
+ color: GREEN,
660
+ fontSize: "0.72rem",
661
+ display: "inline-flex",
662
+ alignItems: "center",
663
+ justifyContent: "center",
664
+ marginTop: "0.05rem"
665
+ };
666
+ var ACTIONS = {
667
+ display: "flex",
668
+ flexWrap: "wrap",
669
+ gap: "0.4rem",
670
+ marginTop: "0.5rem"
671
+ };
672
+ var PRIMARY = {
673
+ padding: "0.35rem 0.65rem",
674
+ borderRadius: 6,
675
+ border: `1px solid rgba(61, 245, 138, 0.5)`,
676
+ background: "rgba(61, 245, 138, 0.14)",
677
+ color: GREEN,
678
+ cursor: "pointer",
679
+ fontSize: "0.76rem",
680
+ fontFamily: MONO2
681
+ };
682
+ var SECONDARY = {
683
+ padding: "0.4rem 0.7rem",
684
+ borderRadius: 6,
685
+ border: `1px solid ${BORDER2}`,
686
+ background: "rgba(255, 255, 255, 0.04)",
687
+ color: "#cfe6db",
688
+ cursor: "pointer",
689
+ fontSize: "0.8rem",
690
+ fontFamily: MONO2
691
+ };
125
692
 
126
693
  // src/WorldEditor.tsx
127
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
694
+ import { OrbitControls, TransformControls } from "@react-three/drei";
695
+ import { Leva, useControls } from "leva";
696
+ import { useEffect as useEffect3, useRef, useState as useState2 } from "react";
697
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
128
698
  var NON_SELECTABLE = /* @__PURE__ */ new Set(["Sky", "LightRig"]);
129
699
  var SKIPPED = /* @__PURE__ */ new Set(["Player"]);
130
700
  var HISTORY_LIMIT = 100;
131
701
  var asVec3 = (value) => Array.isArray(value) && value.length === 3 ? value : void 0;
702
+ var needsIds = (nodes) => nodes.some((node) => !node.id || (node.children ? needsIds(node.children) : false));
703
+ var stripIds = (node) => ({
704
+ ...node,
705
+ id: void 0,
706
+ ...node.children ? { children: node.children.map(stripIds) } : {}
707
+ });
132
708
  var isTyping = (target) => {
133
709
  const el = target;
134
710
  return !!el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable);
135
711
  };
136
712
  function WorldEditor({ data, registry, onChange, ...worldProps }) {
137
- const [selected, setSelected] = useState(null);
138
- const [mode, setMode] = useState("translate");
713
+ const [selected, setSelected] = useState2(null);
714
+ const [mode, setMode] = useState2("translate");
715
+ const [contributeOpen, setContributeOpen] = useState2(false);
139
716
  const history = useRef([]);
717
+ const canContribute = !!data.meta?.source?.url;
140
718
  const apply = (next) => {
141
719
  history.current.push(data);
142
720
  if (history.current.length > HISTORY_LIMIT) history.current.shift();
@@ -155,16 +733,18 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
155
733
  apply({ ...data, nodes });
156
734
  };
157
735
  const addNode = (type) => {
158
- apply({ ...data, nodes: [...data.nodes, { type, props: { position: [0, 0, 0] } }] });
736
+ apply(
737
+ assignNodeIds({ ...data, nodes: [...data.nodes, { type, props: { position: [0, 0, 0] } }] })
738
+ );
159
739
  setSelected(null);
160
740
  };
161
741
  const duplicateSelected = () => {
162
742
  if (!selected) return;
163
743
  const source = data.nodes[selected.index];
164
- const copy = JSON.parse(JSON.stringify(source));
744
+ const copy = stripIds(JSON.parse(JSON.stringify(source)));
165
745
  const at = asVec3(copy.props?.position) ?? [0, 0, 0];
166
746
  copy.props = { ...copy.props, position: [at[0] + 0.5, at[1], at[2] + 0.5] };
167
- apply({ ...data, nodes: [...data.nodes, copy] });
747
+ apply(assignNodeIds({ ...data, nodes: [...data.nodes, copy] }));
168
748
  setSelected(null);
169
749
  };
170
750
  const deleteSelected = () => {
@@ -181,7 +761,10 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
181
761
  rotation: [r(rotation.x), r(rotation.y), r(rotation.z)]
182
762
  });
183
763
  };
184
- useEffect(() => {
764
+ useEffect3(() => {
765
+ if (needsIds(data.nodes)) onChange(assignNodeIds(data));
766
+ }, [data, onChange]);
767
+ useEffect3(() => {
185
768
  const onKey = (event) => {
186
769
  if (isTyping(event.target)) return;
187
770
  if ((event.metaKey || event.ctrlKey) && event.key === "z") {
@@ -198,8 +781,8 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
198
781
  window.addEventListener("keydown", onKey);
199
782
  return () => window.removeEventListener("keydown", onKey);
200
783
  }, [data, selected, onChange]);
201
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
202
- /* @__PURE__ */ jsxs2(
784
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
785
+ /* @__PURE__ */ jsxs4(
203
786
  World,
204
787
  {
205
788
  ...worldProps,
@@ -207,10 +790,14 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
207
790
  gravity: data.gravity,
208
791
  palette: data.palette,
209
792
  fog: data.fog,
793
+ time: data.time,
794
+ timezone: data.timezone,
795
+ avatar: data.avatar,
796
+ preserveDrawingBuffer: canContribute,
210
797
  onPointerMissed: () => setSelected(null),
211
798
  children: [
212
- /* @__PURE__ */ jsx2(OrbitControls, { makeDefault: true }),
213
- /* @__PURE__ */ jsx2(
799
+ /* @__PURE__ */ jsx4(OrbitControls, { makeDefault: true }),
800
+ /* @__PURE__ */ jsx4(
214
801
  EditableNodes,
215
802
  {
216
803
  nodes: data.nodes,
@@ -218,12 +805,14 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
218
805
  onSelect: (index, object) => setSelected({ index, object })
219
806
  }
220
807
  ),
221
- selected && /* @__PURE__ */ jsx2(TransformControls, { object: selected.object, mode, onMouseUp: commitTransform })
808
+ selected && /* @__PURE__ */ jsx4(TransformControls, { object: selected.object, mode, onMouseUp: commitTransform })
222
809
  ]
223
810
  }
224
811
  ),
225
- /* @__PURE__ */ jsx2(Leva, { hidden: selected === null }),
226
- /* @__PURE__ */ jsx2(
812
+ /* @__PURE__ */ jsx4(Leva, { hidden: selected === null }),
813
+ /* @__PURE__ */ jsx4(WorldAbout, { meta: data.meta }),
814
+ contributeOpen && /* @__PURE__ */ jsx4(WorldContribute, { data, onClose: () => setContributeOpen(false) }),
815
+ /* @__PURE__ */ jsx4(
227
816
  EditorToolbar,
228
817
  {
229
818
  mode,
@@ -232,13 +821,15 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
232
821
  registry,
233
822
  selected: selected?.index ?? null,
234
823
  canUndo: history.current.length > 0,
824
+ canContribute,
235
825
  onAdd: addNode,
236
826
  onDuplicate: duplicateSelected,
237
827
  onDelete: deleteSelected,
238
- onUndo: undo
828
+ onUndo: undo,
829
+ onContribute: () => setContributeOpen(true)
239
830
  }
240
831
  ),
241
- selected !== null && /* @__PURE__ */ jsx2(
832
+ selected !== null && /* @__PURE__ */ jsx4(
242
833
  NodeControls,
243
834
  {
244
835
  index: selected.index,
@@ -249,17 +840,17 @@ function WorldEditor({ data, registry, onChange, ...worldProps }) {
249
840
  ] });
250
841
  }
251
842
  function EditableNodes({ nodes, registry, onSelect }) {
252
- return /* @__PURE__ */ jsx2(Fragment2, { children: nodes.map((node, index) => {
843
+ return /* @__PURE__ */ jsx4(Fragment4, { children: nodes.map((node, index) => {
253
844
  if (SKIPPED.has(node.type)) return null;
254
845
  const Component = registry[node.type];
255
846
  if (!Component) return null;
256
847
  if (NON_SELECTABLE.has(node.type)) {
257
- return /* @__PURE__ */ jsx2(Component, { ...node.props }, index);
848
+ return /* @__PURE__ */ jsx4(Component, { ...node.props }, node.id ?? index);
258
849
  }
259
850
  const { position, rotation, ...rest } = node.props ?? {};
260
851
  return (
261
852
  // biome-ignore lint/a11y/noStaticElementInteractions: <group> is a three.js object, not a DOM element
262
- /* @__PURE__ */ jsx2(
853
+ /* @__PURE__ */ jsx4(
263
854
  "group",
264
855
  {
265
856
  position: asVec3(position) ?? [0, 0, 0],
@@ -268,9 +859,9 @@ function EditableNodes({ nodes, registry, onSelect }) {
268
859
  event.stopPropagation();
269
860
  onSelect(index, event.eventObject);
270
861
  },
271
- children: /* @__PURE__ */ jsx2(Component, { ...rest })
862
+ children: /* @__PURE__ */ jsx4(Component, { ...rest })
272
863
  },
273
- index
864
+ node.id ?? index
274
865
  )
275
866
  );
276
867
  }) });
@@ -342,12 +933,12 @@ var SELECT = {
342
933
  appearance: "none",
343
934
  paddingRight: "0.9rem"
344
935
  };
345
- var DIVIDER = {
936
+ var DIVIDER2 = {
346
937
  width: 1,
347
938
  alignSelf: "stretch",
348
939
  background: "#15202a"
349
940
  };
350
- var HINT = {
941
+ var HINT2 = {
351
942
  color: "#5f7d75",
352
943
  fontSize: "0.72rem",
353
944
  paddingLeft: "0.2rem"
@@ -359,10 +950,12 @@ function EditorToolbar({
359
950
  registry,
360
951
  selected,
361
952
  canUndo,
953
+ canContribute,
362
954
  onAdd,
363
955
  onDuplicate,
364
956
  onDelete,
365
- onUndo
957
+ onUndo,
958
+ onContribute
366
959
  }) {
367
960
  const hasSelection = selected !== null;
368
961
  const exportWorld = () => {
@@ -374,8 +967,8 @@ function EditorToolbar({
374
967
  a.click();
375
968
  URL.revokeObjectURL(url);
376
969
  };
377
- return /* @__PURE__ */ jsxs2("div", { style: TOOLBAR, children: [
378
- /* @__PURE__ */ jsx2(
970
+ return /* @__PURE__ */ jsxs4("div", { style: TOOLBAR, children: [
971
+ /* @__PURE__ */ jsx4(
379
972
  "button",
380
973
  {
381
974
  type: "button",
@@ -385,7 +978,7 @@ function EditorToolbar({
385
978
  children: "Move"
386
979
  }
387
980
  ),
388
- /* @__PURE__ */ jsx2(
981
+ /* @__PURE__ */ jsx4(
389
982
  "button",
390
983
  {
391
984
  type: "button",
@@ -395,8 +988,8 @@ function EditorToolbar({
395
988
  children: "Rotate"
396
989
  }
397
990
  ),
398
- /* @__PURE__ */ jsx2("span", { style: DIVIDER }),
399
- /* @__PURE__ */ jsxs2(
991
+ /* @__PURE__ */ jsx4("span", { style: DIVIDER2 }),
992
+ /* @__PURE__ */ jsxs4(
400
993
  "select",
401
994
  {
402
995
  style: SELECT,
@@ -407,12 +1000,12 @@ function EditorToolbar({
407
1000
  },
408
1001
  title: "Insert a component at the origin",
409
1002
  children: [
410
- /* @__PURE__ */ jsx2("option", { value: "", children: "+ Add\u2026" }),
411
- Object.keys(registry).sort().map((name) => /* @__PURE__ */ jsx2("option", { value: name, children: name }, name))
1003
+ /* @__PURE__ */ jsx4("option", { value: "", children: "+ Add\u2026" }),
1004
+ Object.keys(registry).sort().map((name) => /* @__PURE__ */ jsx4("option", { value: name, children: name }, name))
412
1005
  ]
413
1006
  }
414
1007
  ),
415
- /* @__PURE__ */ jsx2(
1008
+ /* @__PURE__ */ jsx4(
416
1009
  "button",
417
1010
  {
418
1011
  type: "button",
@@ -423,7 +1016,7 @@ function EditorToolbar({
423
1016
  children: "Duplicate"
424
1017
  }
425
1018
  ),
426
- /* @__PURE__ */ jsx2(
1019
+ /* @__PURE__ */ jsx4(
427
1020
  "button",
428
1021
  {
429
1022
  type: "button",
@@ -434,7 +1027,7 @@ function EditorToolbar({
434
1027
  children: "Delete"
435
1028
  }
436
1029
  ),
437
- /* @__PURE__ */ jsx2(
1030
+ /* @__PURE__ */ jsx4(
438
1031
  "button",
439
1032
  {
440
1033
  type: "button",
@@ -445,55 +1038,85 @@ function EditorToolbar({
445
1038
  children: "Undo"
446
1039
  }
447
1040
  ),
448
- /* @__PURE__ */ jsx2("span", { style: DIVIDER }),
449
- /* @__PURE__ */ jsx2("button", { type: "button", style: button(false), onClick: exportWorld, children: "Download JSON" }),
450
- /* @__PURE__ */ jsx2("span", { style: HINT, children: selected === null ? "click a component \xB7 Esc deselects" : `selected #${selected}` })
1041
+ /* @__PURE__ */ jsx4("span", { style: DIVIDER2 }),
1042
+ /* @__PURE__ */ jsx4("button", { type: "button", style: button(false), onClick: exportWorld, children: "Download JSON" }),
1043
+ canContribute && /* @__PURE__ */ jsx4(
1044
+ "button",
1045
+ {
1046
+ type: "button",
1047
+ style: button(false),
1048
+ onClick: onContribute,
1049
+ title: "Fork or suggest changes upstream",
1050
+ children: "Contribute \u2197"
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsx4("span", { style: HINT2, children: selected === null ? "click a component \xB7 Esc deselects" : `selected #${selected}` })
451
1054
  ] });
452
1055
  }
453
1056
 
454
1057
  // src/WorldNodes.tsx
455
- import { Fragment as Fragment3, jsx as jsx3 } from "react/jsx-runtime";
1058
+ import { Fragment as Fragment5, jsx as jsx5 } from "react/jsx-runtime";
456
1059
  function WorldNodes({ nodes, registry }) {
457
- return /* @__PURE__ */ jsx3(Fragment3, { children: nodes.map((node, index) => {
1060
+ return /* @__PURE__ */ jsx5(Fragment5, { children: nodes.map((node, index) => {
458
1061
  const Component = registry[node.type];
459
1062
  if (!Component) {
460
1063
  console.warn(`[runek] Unknown component "${node.type}" \u2014 skipped.`);
461
1064
  return null;
462
1065
  }
463
- const children = node.children?.length ? /* @__PURE__ */ jsx3(WorldNodes, { nodes: node.children, registry }) : null;
464
- return /* @__PURE__ */ jsx3(Component, { ...node.props, children }, index);
1066
+ const children = node.children?.length ? /* @__PURE__ */ jsx5(WorldNodes, { nodes: node.children, registry }) : null;
1067
+ return /* @__PURE__ */ jsx5(Component, { ...node.props, children }, node.id ?? index);
465
1068
  }) });
466
1069
  }
467
1070
 
468
1071
  // src/WorldRenderer.tsx
469
- import { jsx as jsx4 } from "react/jsx-runtime";
1072
+ import { Fragment as Fragment6, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
470
1073
  function WorldRenderer({ data, registry, ...worldProps }) {
471
- return /* @__PURE__ */ jsx4(
472
- World,
473
- {
474
- unit: data.unit,
475
- gravity: data.gravity,
476
- palette: data.palette,
477
- fog: data.fog,
478
- ...worldProps,
479
- children: /* @__PURE__ */ jsx4(WorldNodes, { nodes: data.nodes, registry })
480
- }
481
- );
1074
+ return /* @__PURE__ */ jsxs5(Fragment6, { children: [
1075
+ /* @__PURE__ */ jsx6(
1076
+ World,
1077
+ {
1078
+ unit: data.unit,
1079
+ gravity: data.gravity,
1080
+ palette: data.palette,
1081
+ fog: data.fog,
1082
+ time: data.time,
1083
+ timezone: data.timezone,
1084
+ avatar: data.avatar,
1085
+ ...worldProps,
1086
+ children: /* @__PURE__ */ jsx6(WorldNodes, { nodes: data.nodes, registry })
1087
+ }
1088
+ ),
1089
+ /* @__PURE__ */ jsx6(WorldAbout, { meta: data.meta })
1090
+ ] });
482
1091
  }
483
1092
  export {
1093
+ DEFAULT_FONT,
1094
+ DEFAULT_FONTS,
484
1095
  DEFAULT_PALETTE,
1096
+ DEFAULT_WORLD_TIME,
485
1097
  World,
1098
+ WorldAbout,
486
1099
  WorldContext,
1100
+ WorldContribute,
487
1101
  WorldEditor,
488
1102
  WorldNodes,
489
1103
  WorldRenderer,
1104
+ assignNodeIds,
1105
+ clockHours,
1106
+ currentHours,
1107
+ editFileUrl,
1108
+ forkUrl,
490
1109
  int,
491
1110
  keyboardMap,
1111
+ parseClockTime,
1112
+ parseGitHubSource,
492
1113
  parseWorld,
493
1114
  pick,
494
1115
  range,
1116
+ resolveWorldTime,
495
1117
  rng,
496
1118
  serializeWorld,
497
1119
  sub,
1120
+ sunState,
498
1121
  useWorld
499
1122
  };