blockyard 0.0.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +359 -1
- package/README.md +48 -27
- package/SECURITY.md +2 -2
- package/bin/blockyard.js +2 -1
- package/docs/API.md +17 -15
- package/docs/ARCHITECTURE.md +128 -10
- package/docs/CONFIGURATION.md +39 -30
- package/docs/DEFECTS.md +4 -1
- package/docs/GETTING-STARTED.md +18 -8
- package/docs/INSTALL.md +97 -37
- package/docs/MEASUREMENTS.md +147 -0
- package/docs/PLAN-SCORCHED-YARD.md +456 -0
- package/docs/PLAN-SKIES.md +142 -0
- package/docs/SECURITY-AUDIT-2026-09-16.md +647 -0
- package/docs/SECURITY.md +58 -22
- package/docs/TROUBLESHOOTING.md +44 -5
- package/docs/USER-GUIDE.md +505 -35
- package/package.json +4 -2
- package/public/404.html +1 -1
- package/public/css/app.css +393 -82
- package/public/donate-qr.png +0 -0
- package/public/index.html +353 -109
- package/public/js/agents.js +228 -51
- package/public/js/app.js +131 -16
- package/public/js/blockanoid.js +15 -7
- package/public/js/blockout.js +15 -7
- package/public/js/blockscene3d.js +230 -38
- package/public/js/charts.js +21 -21
- package/public/js/depthchart.js +31 -27
- package/public/js/details3d.js +1481 -73
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +7 -1
- package/public/js/livingsky.js +494 -0
- package/public/js/login.js +8 -2
- package/public/js/markets.js +46 -8
- package/public/js/mining.js +314 -36
- package/public/js/panels.js +41 -28
- package/public/js/pricechart.js +14 -13
- package/public/js/quake.js +20 -0
- package/public/js/safenext.js +14 -0
- package/public/js/scorched.js +1051 -0
- package/public/js/scorchedai.js +227 -0
- package/public/js/scorchedair.js +286 -0
- package/public/js/scorchedfx.js +376 -0
- package/public/js/scorchedshop.js +105 -0
- package/public/js/scorchedwind.js +69 -0
- package/public/js/scorchedyard.js +1338 -0
- package/public/js/settings.js +368 -100
- package/public/js/soundcard.js +459 -0
- package/public/js/tetrust.js +15 -6
- package/public/js/tetsound.js +35 -5
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/scripts/check.js +46 -0
- package/scripts/donate-qr.py +12 -9
- package/scripts/dos-bench.js +56 -0
- package/scripts/index-build.js +9 -2
- package/scripts/pool-map.js +152 -36
- package/scripts/setup.js +142 -22
- package/scripts/shots.mjs +27 -0
- package/scripts/smoke.sh +7 -6
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +4 -2
- package/server/auth/sessions.js +33 -13
- package/server/chain/blockfile.js +64 -5
- package/server/chain/index/build.js +451 -58
- package/server/chain/index/heights.js +29 -3
- package/server/chain/index/live.js +13 -7
- package/server/chain/index/rows.js +6 -1
- package/server/chain/index/store.js +28 -5
- package/server/chain/index/worker.js +23 -11
- package/server/collect/logparse.js +65 -18
- package/server/collect/markets.js +76 -7
- package/server/collect/mining.js +32 -0
- package/server/collect/monitor.js +54 -12
- package/server/collect/network.js +305 -0
- package/server/config.js +53 -22
- package/server/http/api.js +119 -18
- package/server/http/games.js +77 -0
- package/server/http/server.js +30 -5
- package/server/http/sse.js +53 -7
- package/server/main.js +66 -11
- package/server/rpc/allowlist.js +26 -0
- package/server/rpc/client.js +30 -2
- package/server/store/audit.js +6 -1
- package/server/store/history.js +19 -3
- package/server/store/ledger.js +15 -4
- package/server/tls/selfsigned.js +160 -0
- package/systemd/blockyard.service +41 -8
- package/docs/PRIVATE-LEADERBOARD.md +0 -230
- package/docs/STATE-2026-09-09.md +0 -200
package/public/js/agents.js
CHANGED
|
@@ -268,6 +268,91 @@ function bloom(ctx, x, y, r, rgb, a = 1) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* THE SAUCER, drawn at a point on screen -- shared, because two effects fly the same craft
|
|
273
|
+
* (operator, 2026-09-15: "The top-down view in block space coming from a sphere does not look
|
|
274
|
+
* good. Can we re-purpose the UFO"). The scan beam used to hang from a glowing ball, which from
|
|
275
|
+
* directly above is just a bright blob; a craft reads as a source even top-down, because its rim
|
|
276
|
+
* and its dome give it an orientation a sphere does not have.
|
|
277
|
+
*
|
|
278
|
+
* `r` is in PIXELS: callers size it from view.unit, never from lw (see bloom).
|
|
279
|
+
*/
|
|
280
|
+
/**
|
|
281
|
+
* THE SAUCER SEEN FROM ABOVE (operator, 2026-09-15: "We need a top-down rendering of the UFO for
|
|
282
|
+
* this scene"). The side-on craft below is right where the camera looks along the board; on the
|
|
283
|
+
* block board it looks down, and a dome-and-rim drawn edge-on there is just a blue smear -- which
|
|
284
|
+
* is what the screenshot showed.
|
|
285
|
+
*
|
|
286
|
+
* From above a saucer is concentric: hull, inner ring, canopy, and running lights spaced around
|
|
287
|
+
* the rim. `squash` is the camera's depth foreshortening (oblique.dy), so the circles come out as
|
|
288
|
+
* the same ellipses the board itself is drawn in.
|
|
289
|
+
*/
|
|
290
|
+
export function saucerAbove(ctx, cx, cy, r, a = 1, squash = 1, phase = 0) {
|
|
291
|
+
const ell = (rr, fill) => {
|
|
292
|
+
ctx.fillStyle = fill;
|
|
293
|
+
ctx.beginPath();
|
|
294
|
+
for (let i = 0; i <= 28; i++) {
|
|
295
|
+
const th = (i / 28) * Math.PI * 2;
|
|
296
|
+
const x = cx + Math.cos(th) * rr, y = cy + Math.sin(th) * rr * squash;
|
|
297
|
+
if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y);
|
|
298
|
+
}
|
|
299
|
+
ctx.closePath(); ctx.fill();
|
|
300
|
+
};
|
|
301
|
+
ell(r, `rgba(70,110,170,${(0.85 * a).toFixed(3)})`); // the hull, dark against the glow
|
|
302
|
+
ell(r * 0.74, `rgba(120,180,255,${(0.9 * a).toFixed(3)})`); // the lit ring
|
|
303
|
+
ell(r * 0.40, `rgba(205,235,255,${(0.95 * a).toFixed(3)})`); // the canopy
|
|
304
|
+
ell(r * 0.18, `rgba(255,255,255,${(0.95 * a).toFixed(3)})`); // its highlight
|
|
305
|
+
// running lights: spaced round the rim, turning slowly so it reads as a craft and not a target
|
|
306
|
+
for (let i = 0; i < 8; i++) {
|
|
307
|
+
const th = phase + (i / 8) * Math.PI * 2;
|
|
308
|
+
const x = cx + Math.cos(th) * r * 0.88, y = cy + Math.sin(th) * r * 0.88 * squash;
|
|
309
|
+
const tw = 0.55 + 0.45 * Math.sin(phase * 3 + i * 1.9);
|
|
310
|
+
ctx.fillStyle = `rgba(255,255,255,${(0.9 * tw * a).toFixed(3)})`;
|
|
311
|
+
ctx.beginPath(); ctx.arc(x, y, Math.max(0.6, r * 0.075), 0, Math.PI * 2); ctx.fill();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function saucer(ctx, cx, cy, r, a = 1) {
|
|
316
|
+
const dome = [];
|
|
317
|
+
for (let i = 0; i <= 20; i++) { const ang = Math.PI + (i / 20) * Math.PI; dome.push({ x: cx + Math.cos(ang) * r, y: cy + Math.sin(ang) * r * 0.42 }); }
|
|
318
|
+
poly(ctx, dome, `rgba(120,180,255,${(0.9 * a).toFixed(3)})`);
|
|
319
|
+
poly(ctx, [
|
|
320
|
+
{ x: cx - r, y: cy }, { x: cx + r, y: cy },
|
|
321
|
+
{ x: cx + r * 0.7, y: cy + r * 0.25 }, { x: cx - r * 0.7, y: cy + r * 0.25 },
|
|
322
|
+
], `rgba(200,230,255,${(0.95 * a).toFixed(3)})`);
|
|
323
|
+
bloom(ctx, cx, cy + r * 0.2, r * 0.8, [180, 230, 255], 0.5 * a);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A lens flare at a bright point: an anamorphic streak in the colour, a white core, four thin
|
|
328
|
+
* rays turning slowly, and a run of faint ghosts along the line toward the picture's middle.
|
|
329
|
+
* `f` is 1 at full and 0 gone. Gradients only, no composite modes (the canvas rules).
|
|
330
|
+
*/
|
|
331
|
+
export function lensFlare(ctx, x, y, L, rgb, f, view, lw) {
|
|
332
|
+
if (f <= 0) return;
|
|
333
|
+
const c = rgb.join(',');
|
|
334
|
+
const lin = (x0, y0, x1, y1, stops) => {
|
|
335
|
+
const g = typeof ctx.createLinearGradient === 'function' ? ctx.createLinearGradient(x0, y0, x1, y1) : null;
|
|
336
|
+
if (!g || typeof g.addColorStop !== 'function') return stops[Math.floor(stops.length / 2)][1];
|
|
337
|
+
for (const [o, col] of stops) g.addColorStop(o, col);
|
|
338
|
+
return g;
|
|
339
|
+
};
|
|
340
|
+
const stroke = (x0, y0, x1, y1, fill, w) => { ctx.strokeStyle = fill; ctx.lineWidth = Math.max(lw * 0.8, w); ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); };
|
|
341
|
+
stroke(x - L * 0.7, y, x + L * 0.7, y, lin(x - L * 0.7, y, x + L * 0.7, y, [[0, `rgba(${c},0)`], [0.5, `rgba(${c},${(0.25 * f).toFixed(3)})`], [1, `rgba(${c},0)`]]), L * 0.06);
|
|
342
|
+
stroke(x - L, y, x + L, y, lin(x - L, y, x + L, y, [[0, `rgba(${c},0)`], [0.35, `rgba(${c},${(0.35 * f).toFixed(3)})`], [0.5, `rgba(255,255,255,${(0.9 * f).toFixed(3)})`], [0.65, `rgba(${c},${(0.35 * f).toFixed(3)})`], [1, `rgba(${c},0)`]]), L * 0.018);
|
|
343
|
+
const spin = (view.now ?? 0) * 0.0004;
|
|
344
|
+
for (let k = 0; k < 4; k++) {
|
|
345
|
+
const ang = spin + (k / 4) * Math.PI, rl = L * 0.45;
|
|
346
|
+
const x0 = x - Math.cos(ang) * rl, y0 = y - Math.sin(ang) * rl, x1 = x + Math.cos(ang) * rl, y1 = y + Math.sin(ang) * rl;
|
|
347
|
+
stroke(x0, y0, x1, y1, lin(x0, y0, x1, y1, [[0, 'rgba(255,255,255,0)'], [0.5, `rgba(255,255,255,${(0.5 * f).toFixed(3)})`], [1, 'rgba(255,255,255,0)']]), L * 0.008);
|
|
348
|
+
}
|
|
349
|
+
const cx0 = (view.boardW ?? 0) / 2, cy0 = -(view.boardH ?? 0) / 4;
|
|
350
|
+
for (let k = 1; k <= 4; k++) {
|
|
351
|
+
const t = k * 0.3, gx = x + (cx0 - x) * t, gy = y + (cy0 - y) * t, gr = L * 0.05 * (0.6 + t), ga = 0.14 * f * (1 - k / 6);
|
|
352
|
+
ctx.fillStyle = `rgba(${c},${ga.toFixed(3)})`; ctx.beginPath(); ctx.arc(gx, gy, Math.max(1, gr), 0, Math.PI * 2); ctx.fill();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
271
356
|
/** A square standing at grid (x, y) at height z, as screen points. */
|
|
272
357
|
const quad = (view, x, y, z, r) => [
|
|
273
358
|
project(x - r, y - r, z, view), project(x + r, y - r, z, view),
|
|
@@ -504,16 +589,8 @@ defineAgent('tractor', {
|
|
|
504
589
|
{ x: base.x + w1 * 0.45, y: base.y }, { x: base.x - w1 * 0.45, y: base.y },
|
|
505
590
|
], `rgba(220,245,255,${(0.3 * t.beam).toFixed(3)})`);
|
|
506
591
|
}
|
|
507
|
-
// the saucer
|
|
508
|
-
|
|
509
|
-
const dome = [];
|
|
510
|
-
for (let i = 0; i <= 20; i++) { const ang = Math.PI + (i / 20) * Math.PI; dome.push({ x: c.x + Math.cos(ang) * r, y: c.y + Math.sin(ang) * r * 0.42 }); }
|
|
511
|
-
poly(ctx, dome, 'rgba(120,180,255,0.9)');
|
|
512
|
-
poly(ctx, [
|
|
513
|
-
{ x: c.x - r, y: c.y }, { x: c.x + r, y: c.y },
|
|
514
|
-
{ x: c.x + r * 0.7, y: c.y + r * 0.25 }, { x: c.x - r * 0.7, y: c.y + r * 0.25 },
|
|
515
|
-
], 'rgba(200,230,255,0.95)');
|
|
516
|
-
bloom(ctx, c.x, c.y + r * 0.2, r * 0.8, [180, 230, 255], 0.5);
|
|
592
|
+
// the saucer, shared with the scan beam's source (saucer() above)
|
|
593
|
+
saucer(ctx, c.x, c.y, U * 1.6);
|
|
517
594
|
void lw;
|
|
518
595
|
},
|
|
519
596
|
});
|
|
@@ -721,11 +798,19 @@ defineAgent('stormball', {
|
|
|
721
798
|
// range, so the candles it passes are beside it and the arcs strike what it comes near
|
|
722
799
|
// (operator, 2026-09-14: "electrifying any elements it comes near")
|
|
723
800
|
const line = st?.axes?.line;
|
|
724
|
-
let alt = highest + 3.5;
|
|
801
|
+
let alt = highest + 3.5, zPath = null;
|
|
725
802
|
if (line?.length > 1) {
|
|
726
|
-
let
|
|
727
|
-
for (const p of line)
|
|
728
|
-
|
|
803
|
+
let hi = -Infinity;
|
|
804
|
+
for (const p of line) hi = Math.max(hi, p.z);
|
|
805
|
+
// UP AND DOWN THE WHOLE CHART (operator, 2026-09-15: "it's not moving up and down enough
|
|
806
|
+
// during transit. It should try to cover a lot of space"): the height is two sines over the
|
|
807
|
+
// run, the slow one swinging across most of the range, the quick one on top. The range is
|
|
808
|
+
// the ground to the line's high -- not the line's own low, which sits above the volume band
|
|
809
|
+
// (operator, later: "The lightning ball really stays in the top 2/3's of the screen. It
|
|
810
|
+
// needs to seek the ground level more than it presently does before rising again")
|
|
811
|
+
const lo = 1;
|
|
812
|
+
alt = (lo + hi) / 2;
|
|
813
|
+
zPath = { lo, hi, f: [1.2 + rnd() * 1.3, 3 + rnd() * 3], ph: [rnd() * Math.PI * 2, rnd() * Math.PI * 2] };
|
|
729
814
|
}
|
|
730
815
|
// across the board the long way round, far enough past both edges to start and end off-screen
|
|
731
816
|
const leftToRight = rnd() < 0.5;
|
|
@@ -736,35 +821,49 @@ defineAgent('stormball', {
|
|
|
736
821
|
// "have it fully moved off the display before you remove it"): the corona reaches 12 units
|
|
737
822
|
// from the centre and the trail seven behind, and at 8 the run ended with the glow still
|
|
738
823
|
// on the chart and cut off dead
|
|
739
|
-
const margin = st?.axes?.line?.length > 1 ?
|
|
824
|
+
const margin = st?.axes?.line?.length > 1 ? 20 : Math.max(W, H) * 0.55 + 6;
|
|
740
825
|
const from = { x: leftToRight ? -margin : W + margin, y: H * (0.22 + 0.56 * rnd()) };
|
|
741
826
|
const to = { x: leftToRight ? W + margin : -margin, y: H * (0.22 + 0.56 * rnd()) };
|
|
742
827
|
const weave = { amp: 1.5 + 2 * rnd(), cycles: 1 + rnd() * 1.5, phase: rnd() * Math.PI * 2 };
|
|
743
|
-
// bursts of arcs: a few a second, one to three at a time, each alive for a moment
|
|
828
|
+
// bursts of arcs: a few a second, one to three at a time, each alive for a moment -- half as
|
|
829
|
+
// often on the price board (operator, 2026-09-15: "Tone down chance of emitting lightning by
|
|
830
|
+
// 50%"), where the run is also slower, so per second it is quieter still
|
|
744
831
|
const arcs = [];
|
|
745
|
-
|
|
832
|
+
// ...a fifth quieter again ("Reduce ball lighting electricity chance by 20%"), then 15% more
|
|
833
|
+
// ("increase the ball lightning strike chance by +15%"): 2.5 / 1.15
|
|
834
|
+
const every = line?.length > 1 ? 2.17 : 1;
|
|
835
|
+
for (let u = 0.03; u < 0.97; u += (0.016 + 0.035 * rnd()) * every) {
|
|
746
836
|
const n = 1 + Math.floor(rnd() * 4);
|
|
747
837
|
for (let k = 0; k < n; k++) {
|
|
748
838
|
const arc = { u0: u + rnd() * 0.01, life: 0.02 + 0.035 * rnd(), ang: rnd() * Math.PI * 2, reach: 3.5 + 8 * rnd(), seed: Math.floor(rnd() * 1e9) };
|
|
839
|
+
// one strike in five flares: a lens flare blooms where it lands (operator, 2026-09-15:
|
|
840
|
+
// "Consider adding the lense flare effect on occasional lightning strikes")
|
|
841
|
+
if (rnd() < 0.2) arc.flare = true;
|
|
749
842
|
// A SECOND ARC OFF THE STRUCK BLOCK, AND SOMETIMES A THIRD (operator, 2026-09-14: "a small
|
|
750
843
|
// chance for a second arc to spawn from the block and arc to a different block", then "Make
|
|
751
844
|
// the secondary arcing more frequent, and have a 50% for an additional third arc"). Half
|
|
752
845
|
// the arcs carry a chain -- its own direction and reach from the block the first lands on,
|
|
753
846
|
// to a different block -- and half of those chain once more from the second block to a
|
|
754
847
|
// third. Decided here, so a replay throws the same chains.
|
|
755
|
-
|
|
848
|
+
// ...then a quarter fewer of each (2026-09-15: "decrease secondary and third arcing by 25%")
|
|
849
|
+
if (rnd() < 0.375) {
|
|
756
850
|
arc.chain = { ang: rnd() * Math.PI * 2, reach: 2.5 + 6 * rnd(), seed: Math.floor(rnd() * 1e9) };
|
|
757
|
-
if (rnd() < 0.
|
|
851
|
+
if (rnd() < 0.375) arc.chain.next = { ang: rnd() * Math.PI * 2, reach: 2.5 + 6 * rnd(), seed: Math.floor(rnd() * 1e9) };
|
|
758
852
|
}
|
|
759
853
|
arcs.push(arc);
|
|
760
854
|
}
|
|
761
855
|
}
|
|
762
|
-
return { from, to, weave, alt, arcs, W, H, top };
|
|
856
|
+
return { from, to, weave, alt, zPath, arcs, W, H, top };
|
|
763
857
|
},
|
|
764
858
|
frame(a, u) {
|
|
765
859
|
const at = (uu) => {
|
|
766
860
|
const x = a.from.x + (a.to.x - a.from.x) * uu;
|
|
767
861
|
const y = a.from.y + (a.to.y - a.from.y) * uu + a.weave.amp * Math.sin(uu * Math.PI * 2 * a.weave.cycles + a.weave.phase);
|
|
862
|
+
if (a.zPath) {
|
|
863
|
+
const { lo, hi, f, ph } = a.zPath;
|
|
864
|
+
const w = 0.5 + 0.44 * Math.sin(uu * Math.PI * 2 * f[0] + ph[0]) + 0.14 * Math.sin(uu * Math.PI * 2 * f[1] + ph[1]);
|
|
865
|
+
return { x, y, z: lo + (hi - lo) * Math.max(0, Math.min(1, w)) };
|
|
866
|
+
}
|
|
768
867
|
return { x, y, z: a.alt + 0.6 * Math.sin(uu * 17) };
|
|
769
868
|
};
|
|
770
869
|
const ball = at(u);
|
|
@@ -790,20 +889,30 @@ defineAgent('stormball', {
|
|
|
790
889
|
return hit;
|
|
791
890
|
};
|
|
792
891
|
const live = [];
|
|
793
|
-
|
|
892
|
+
// LIGHT EMITTERS (operator, 2026-09-15: "The lightning ball needs to illuminate anything it
|
|
893
|
+
// comes near with an electric hazy white effect. Need the lightning strikes to emit lights").
|
|
894
|
+
// `heads` is how an agent lights the board (fxAt): the ball is a wide hazy white source now,
|
|
895
|
+
// not a blue tint, and every strike is a source of its own where it lands
|
|
896
|
+
const heads = [{ x: ball.x, y: ball.y, color: [225, 240, 255], alpha: 0.95, r: 6.5 }];
|
|
794
897
|
for (const arc of a.arcs) {
|
|
795
898
|
const age = (u - arc.u0) / arc.life;
|
|
796
899
|
if (age < 0 || age > 2.2) continue; // alive, then an afterglow on the block
|
|
797
900
|
const p = at(arc.u0);
|
|
901
|
+
// NO ARCS FROM OFF THE BOARD (2026-09-15: "make the lightning ball completely disappear off
|
|
902
|
+
// the viewport before killing it. Seeing it flash out at the end"): a ball past the edge
|
|
903
|
+
// could still reach a candle at the edge with an arc, so the last thing seen was a bolt
|
|
904
|
+
// from nowhere; while it is off the board nothing is drawn from it -- not even an arc it
|
|
905
|
+
// threw a moment before the edge -- and the run ends with it long gone
|
|
906
|
+
if (a.zPath && (ball.x < -1 || ball.x > a.W + 1)) continue;
|
|
798
907
|
// the block the arc strikes: the tallest cell near where it is thrown, and none if the floor is bare
|
|
799
908
|
const hit = strike(p.x + Math.cos(arc.ang) * arc.reach, p.y + Math.sin(arc.ang) * arc.reach);
|
|
800
909
|
if (!hit) continue;
|
|
801
|
-
if (age <= 1) live.push({ to: hit, seed: arc.seed, strength: 1 - age * 0.6 });
|
|
910
|
+
if (age <= 1) live.push({ to: hit, seed: arc.seed, strength: 1 - age * 0.6, age, flare: !!arc.flare });
|
|
802
911
|
// THE BLOCK IT TOUCHES IS ELECTRIFIED: a hard blue flare that flickers while the arc lives and
|
|
803
912
|
// dies away after it -- lit through fxAt's heads, so only the cubes actually struck light up
|
|
804
913
|
const flicker = 0.7 + 0.3 * Math.sin(u * 900 + arc.seed);
|
|
805
914
|
const glow = age <= 1 ? flicker : Math.max(0, 1 - (age - 1) / 1.2) * 0.7;
|
|
806
|
-
heads.push({ x: hit.x, y: hit.y, color: [
|
|
915
|
+
heads.push({ x: hit.x, y: hit.y, color: [230, 245, 255], alpha: Math.min(1, glow * 1.25), r: 2.4 });
|
|
807
916
|
// ...and the chain, if this arc carries one: from the struck block to another. ITS OWN
|
|
808
917
|
// EVENT, not a bend in the first (2026-09-14: "I'm not seeing secondary arcs" -- they were
|
|
809
918
|
// there in a third of the arc frames, drawn in the first arc's colour from the first arc's
|
|
@@ -823,7 +932,7 @@ defineAgent('stormball', {
|
|
|
823
932
|
if (!to) break;
|
|
824
933
|
if (age2 <= 1) live.push({ from, to, seed: link.seed, strength: Math.min(1, 1.2 * (1 - age2 * 0.5)), chain: hop, age: age2 });
|
|
825
934
|
const glow2 = age2 <= 1 ? 0.7 + 0.3 * Math.sin(u * 900 + link.seed) : Math.max(0, 1 - (age2 - 1) / 1.2) * 0.7;
|
|
826
|
-
heads.push({ x: to.x, y: to.y, color: [
|
|
935
|
+
heads.push({ x: to.x, y: to.y, color: [200, 255, 225], alpha: Math.min(1, glow2 * 1.25), r: 2.2 });
|
|
827
936
|
struck.push(to); from = to; link = link.next; hop++;
|
|
828
937
|
}
|
|
829
938
|
}
|
|
@@ -838,42 +947,110 @@ defineAgent('stormball', {
|
|
|
838
947
|
// bigger than the first cut on the block board: it is the one thing there (operator: "visually
|
|
839
948
|
// stunning"); half that on the price board (operator, 2026-09-14: "make the ball lightning half
|
|
840
949
|
// the size it is now"), where it shares the view with the line
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
950
|
+
// ...then half again (2026-09-15: "the lightning ball is still too large. shrink it up by half again")
|
|
951
|
+
const R = U * (view.axes?.line?.length > 1 ? 0.85 : 3.4);
|
|
952
|
+
// LIGHTNING THAT LOOKS LIKE LIGHTNING (operator, 2026-09-15: "the lightning ball tendrils on
|
|
953
|
+
// the markets page look too big and jagged. Is there a way to thin them up a bit, or make
|
|
954
|
+
// substantial visual improvements to the lightning arcs?"). The first bolt was nine points
|
|
955
|
+
// thrown 18% of the length to either side, re-rolled every frame, in strokes sized to the grid
|
|
956
|
+
// unit -- on the price board, whose unit is three times the block board's, that was a fat
|
|
957
|
+
// zigzag flailing at 60 Hz. Three changes:
|
|
958
|
+
// * MIDPOINT DISPLACEMENT: the channel is split in half, the middle thrown to one side, and
|
|
959
|
+
// each half split again four times over with the throw halving each level -- the shape of
|
|
960
|
+
// a discharge, which wanders on the large scale and crackles on the small, never a zigzag;
|
|
961
|
+
// * BRANCHES: one or two forks leave the channel partway, thinner and shorter, each a
|
|
962
|
+
// smaller discharge of its own, dying out before they reach anything;
|
|
963
|
+
// * HELD FOR A BEAT: the shape is rolled from the arc's seed and the clock in 60 ms steps, so
|
|
964
|
+
// it holds for a few frames and then jumps -- a flicker, not a shiver -- and the strokes
|
|
965
|
+
// are sized to the ball's own radius, so the price board's half-size ball throws half-width
|
|
966
|
+
// bolts.
|
|
967
|
+
const beat = Math.floor((view.now ?? 0) / 60);
|
|
968
|
+
const rollFrom = (seed) => { let x = (seed ^ (beat * 2654435761)) >>> 0 || 1; return () => ((x = (Math.imul(x, 1103515245) + 12345) >>> 0) / 4294967296); };
|
|
969
|
+
const displace = (pts, i, j, amp, depth, rnd) => {
|
|
970
|
+
if (depth === 0 || j - i < 2) return;
|
|
971
|
+
const m = (i + j) >> 1, a = pts[i], b = pts[j];
|
|
972
|
+
const nx = -(b.y - a.y), ny = b.x - a.x, L = Math.hypot(nx, ny) || 1;
|
|
973
|
+
const off = (rnd() - 0.5) * 2 * amp * L;
|
|
974
|
+
pts[m] = { x: (a.x + b.x) / 2 + (nx / L) * off, y: (a.y + b.y) / 2 + (ny / L) * off };
|
|
975
|
+
displace(pts, i, m, amp * 0.55, depth - 1, rnd); displace(pts, m, j, amp * 0.55, depth - 1, rnd);
|
|
976
|
+
};
|
|
977
|
+
const bolt = (p, q, amp, rnd, levels = 4) => {
|
|
978
|
+
const n = 1 << levels, pts = new Array(n + 1); pts[0] = p; pts[n] = q;
|
|
979
|
+
displace(pts, 0, n, amp, levels, rnd);
|
|
851
980
|
return pts;
|
|
852
981
|
};
|
|
982
|
+
// THE HAZE: a wide, soft, white light round the ball, over the board, so what it passes reads
|
|
983
|
+
// as lit by it and not only tinted -- and the same, smaller, wherever an arc lands
|
|
984
|
+
{
|
|
985
|
+
const g = typeof ctx.createRadialGradient === 'function' ? ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, R * 5) : null;
|
|
986
|
+
if (g && typeof g.addColorStop === 'function') { g.addColorStop(0, 'rgba(235,245,255,0.30)'); g.addColorStop(0.35, 'rgba(225,240,255,0.14)'); g.addColorStop(1, 'rgba(220,235,255,0)'); ctx.fillStyle = g; }
|
|
987
|
+
else ctx.fillStyle = 'rgba(235,245,255,0.08)';
|
|
988
|
+
ctx.beginPath(); ctx.arc(c.x, c.y, R * 5, 0, Math.PI * 2); ctx.fill();
|
|
989
|
+
}
|
|
853
990
|
// the arcs first, so the sphere sits over the roots of its own lightning
|
|
854
991
|
for (const arc of s.arcs) {
|
|
855
992
|
const end = project(arc.to.x, arc.to.y, arc.to.z, view);
|
|
856
993
|
// a chained arc leaves the block the first one struck, not the ball
|
|
857
994
|
const from = arc.from ? project(arc.from.x, arc.from.y, arc.from.z, view) : c;
|
|
858
|
-
const
|
|
995
|
+
const rnd = rollFrom(arc.seed);
|
|
996
|
+
const main = bolt(from, end, 0.11, rnd);
|
|
859
997
|
// a chain (hop 1 or 2 from the block) is electric green, so it reads as the block discharging, not the ball
|
|
860
998
|
const [halo, body, edge] = arc.chain ? ['20,200,110', '110,255,170', '215,255,235'] : ['30,120,255', '60,170,255', '140,220,255'];
|
|
861
|
-
const sz = arc.chain ? 0.85 : 1;
|
|
862
|
-
line(ctx,
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
//
|
|
999
|
+
const sz = (arc.chain ? 0.85 : 1) * R;
|
|
1000
|
+
const stroke = (pts, col, w, a) => line(ctx, pts, `rgba(${col},${a.toFixed(3)})`, Math.max(lw, w));
|
|
1001
|
+
// THE FLASH (2026-09-15: "the lightning bolts still need much more visual flash and effects
|
|
1002
|
+
// to them"): the first fifth of an arc's life is the strike -- the halo blazes at three
|
|
1003
|
+
// times its weight and a white flash blooms where it lands -- then a bead of light runs the
|
|
1004
|
+
// channel from root to tip, sparks spray from the strike point and fall away, and the whole
|
|
1005
|
+
// thing settles to the steady glow. `age` is 0 at the strike and 1 at the end.
|
|
1006
|
+
const age = arc.age ?? (1 - arc.strength) / 0.6;
|
|
1007
|
+
const flash = Math.max(0, 1 - age / 0.2);
|
|
1008
|
+
stroke(main, halo, sz * (0.30 + 0.25 * flash), (0.28 + 0.5 * flash) * arc.strength);
|
|
1009
|
+
stroke(main, body, sz * 0.12, 0.6 * arc.strength);
|
|
1010
|
+
stroke(main, edge, sz * 0.055, 0.95 * arc.strength);
|
|
1011
|
+
stroke(main, '245,252,255', sz * (0.022 + 0.02 * flash), arc.strength);
|
|
1012
|
+
if (flash > 0) bloom(ctx, end.x, end.y, R * 1.6 * (1.2 - flash * 0.5), [255, 255, 255], 0.9 * flash);
|
|
1013
|
+
// the landing lights its surroundings: a hazy white pool that outlives the flash
|
|
1014
|
+
if (age < 0.8) bloom(ctx, end.x, end.y, R * 2.6, [230, 242, 255], 0.35 * (1 - age / 0.8) * arc.strength);
|
|
1015
|
+
// the occasional strike flares: an anamorphic streak through the landing, four turning
|
|
1016
|
+
// rays, and a run of ghosts off toward the picture's middle, over the first third of the arc
|
|
1017
|
+
if (arc.flare && age < 0.35) lensFlare(ctx, end.x, end.y, R * 2.2, arc.chain ? [110, 255, 170] : [140, 220, 255], Math.pow(1 - age / 0.35, 1.5), view, lw);
|
|
1018
|
+
// the bead: a bright knot of light running the channel in the first third of the arc's life
|
|
1019
|
+
const run = Math.min(1, age / 0.35), bi = Math.min(main.length - 1, Math.floor(run * (main.length - 1)));
|
|
1020
|
+
if (age < 0.5) bloom(ctx, main[bi].x, main[bi].y, sz * 0.22, [235, 250, 255], 0.9 * (1 - age));
|
|
1021
|
+
// the sparks: a dozen short lines flung from the strike point, longer and fainter as they go,
|
|
1022
|
+
// drifting down, gone by half the arc's life
|
|
1023
|
+
if (age < 0.55) {
|
|
1024
|
+
const srnd = rollFrom(arc.seed ^ 0x9e3779b9);
|
|
1025
|
+
const life = age / 0.55;
|
|
1026
|
+
for (let k = 0; k < 12; k++) {
|
|
1027
|
+
const ang = srnd() * Math.PI * 2, spd = 0.6 + srnd() * 1.4, len = sz * (0.25 + 0.2 * srnd());
|
|
1028
|
+
const dist = sz * 1.3 * spd * life, drop = sz * 1.2 * life * life;
|
|
1029
|
+
const x1 = end.x + Math.cos(ang) * dist, y1 = end.y + Math.sin(ang) * dist + drop;
|
|
1030
|
+
const x0 = x1 - Math.cos(ang) * len * (1 - life), y0 = y1 - Math.sin(ang) * len * (1 - life) - drop * 0.15;
|
|
1031
|
+
line(ctx, [{ x: x0, y: y0 }, { x: x1, y: y1 }], `rgba(${edge},${(0.9 * (1 - life) * arc.strength).toFixed(3)})`, Math.max(lw, sz * 0.02));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// the branches: two or three leave the channel a third to two thirds along, at 20-45
|
|
1035
|
+
// degrees, a quarter to a half of the remaining length, and fade toward their tips
|
|
1036
|
+
const forks = 2 + (rnd() < 0.5 ? 1 : 0);
|
|
1037
|
+
for (let f = 0; f < forks; f++) {
|
|
1038
|
+
const k = Math.floor(main.length * (0.3 + 0.4 * rnd()));
|
|
1039
|
+
const at = main[k], dx = end.x - at.x, dy = end.y - at.y;
|
|
1040
|
+
const ang = (rnd() < 0.5 ? 1 : -1) * (0.35 + 0.45 * rnd()), len = 0.25 + 0.25 * rnd();
|
|
1041
|
+
const tip = { x: at.x + (dx * Math.cos(ang) - dy * Math.sin(ang)) * len, y: at.y + (dx * Math.sin(ang) + dy * Math.cos(ang)) * len };
|
|
1042
|
+
const br = bolt(at, tip, 0.14, rnd, 3);
|
|
1043
|
+
stroke(br, body, sz * 0.06, 0.35 * arc.strength);
|
|
1044
|
+
stroke(br, edge, sz * 0.028, 0.7 * arc.strength);
|
|
1045
|
+
stroke(br, '245,252,255', sz * 0.012, 0.8 * arc.strength);
|
|
1046
|
+
}
|
|
1047
|
+
// ...and a ring bursting from the block a chain lands on, the moment it lands
|
|
867
1048
|
if (arc.chain && arc.age < 0.6) {
|
|
868
1049
|
const f = arc.age / 0.6;
|
|
869
|
-
ring(ctx, end.x, end.y,
|
|
870
|
-
ring(ctx, end.x, end.y,
|
|
1050
|
+
ring(ctx, end.x, end.y, R * (0.12 + 0.65 * f), `rgba(150,255,190,${(0.9 * (1 - f)).toFixed(3)})`, Math.max(lw * 2, R * 0.035) * (1 - 0.5 * f));
|
|
1051
|
+
ring(ctx, end.x, end.y, R * (0.06 + 0.4 * f), `rgba(235,255,245,${(0.8 * (1 - f)).toFixed(3)})`, Math.max(lw, R * 0.018));
|
|
871
1052
|
}
|
|
872
|
-
|
|
873
|
-
const k = 3 + Math.floor(Math.random() * 4);
|
|
874
|
-
const fork = main[k], forkEnd = { x: fork.x + (Math.random() - 0.5) * U * 3, y: fork.y + (Math.random() - 0.5) * U * 3 };
|
|
875
|
-
line(ctx, bolt(fork, forkEnd, U * 0.5, 4), `rgba(130,215,255,${(0.6 * arc.strength).toFixed(3)})`, Math.max(lw, U * 0.08));
|
|
876
|
-
bloom(ctx, end.x, end.y, U * 2.2, [90, 200, 255], arc.strength);
|
|
1053
|
+
bloom(ctx, end.x, end.y, R * 0.65, arc.chain ? [120, 255, 180] : [90, 200, 255], arc.strength);
|
|
877
1054
|
}
|
|
878
1055
|
// THE SPHERE IS ONE SMOOTH THING (operator, 2026-09-14: "needs a smooth gradient, not different
|
|
879
1056
|
// discs around it. It needs to be visually stunning. Consider adding nebula effects"). Radial
|
|
@@ -938,9 +1115,9 @@ defineAgent('stormball', {
|
|
|
938
1115
|
const ang = Math.random() * Math.PI * 2, r0 = R * (0.35 + 0.5 * Math.random()), r1 = R * (1.05 + 0.55 * Math.random());
|
|
939
1116
|
const p = { x: c.x + Math.cos(ang) * r0, y: c.y + Math.sin(ang) * r0 };
|
|
940
1117
|
const q = { x: c.x + Math.cos(ang + (Math.random() - 0.5) * 0.9) * r1, y: c.y + Math.sin(ang + (Math.random() - 0.5) * 0.9) * r1 };
|
|
941
|
-
const pts = bolt(p, q,
|
|
942
|
-
line(ctx, pts, 'rgba(90,190,255,0.55)', Math.max(lw *
|
|
943
|
-
line(ctx, pts, 'rgba(230,248,255,0.95)', Math.max(lw,
|
|
1118
|
+
const pts = bolt(p, q, 0.16, rollFrom(i * 7919 + 17), 3);
|
|
1119
|
+
line(ctx, pts, 'rgba(90,190,255,0.55)', Math.max(lw * 1.6, R * 0.035));
|
|
1120
|
+
line(ctx, pts, 'rgba(230,248,255,0.95)', Math.max(lw, R * 0.012));
|
|
944
1121
|
}
|
|
945
1122
|
},
|
|
946
1123
|
});
|
package/public/js/app.js
CHANGED
|
@@ -11,17 +11,46 @@ import { renderMiningOverview, renderMining, renderBlockSpace, refreshLabel } fr
|
|
|
11
11
|
import { viewerIdle } from './details3d.js';
|
|
12
12
|
import {
|
|
13
13
|
loadSettings, setSetting, resetSettings, seedSettings, setSettingsPush,
|
|
14
|
-
SETTINGS_KEY, PANEL as SETTINGS_PANEL, formatRangeValue,
|
|
15
|
-
|
|
14
|
+
SETTINGS_KEY, PANEL as SETTINGS_PANEL, formatRangeValue, SKY_BOARDS, SKY_CHOICES } from './settings.js';
|
|
15
|
+
// APPEARANCE (2026-09-16): the theme goes on <html> before the first paint, from the browser's copy
|
|
16
|
+
// of the settings, and follows every change after (followTheme); the cards' swatches are painted
|
|
17
|
+
// through the CSSOM because the CSP refuses a style attribute in markup
|
|
18
|
+
import { TAB_ROWS } from './settings.js';
|
|
19
|
+
import { followTheme, presetFace, resolveScheme, resolveTheme, PRESETS, BASE_KEYS } from './theme.js';
|
|
16
20
|
import { renderExplorer } from './explorer.js';
|
|
17
21
|
import { renderMarkets, summaryHtml as marketsSummaryHtml, REFRESH_MS as MARKETS_REFRESH_MS } from './markets.js';
|
|
18
22
|
import { renderKiosk } from './kiosk.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
// THE GAMES LOAD WHEN THEY ARE OPENED (the outside review of 2026-09-16: "Move the DOOM/Wolfenstein/
|
|
24
|
+
// Quake emulator and 3D games into an optional, separately loadable module ... This reduces the
|
|
25
|
+
// attack and bug surface of the core monitor"). They were static imports, so the Overview of a node
|
|
26
|
+
// monitor fetched and evaluated twenty game modules -- a quarter of the app's code -- before it drew
|
|
27
|
+
// a chart. Each game is now an import() made the first time its page is opened; a monitor that is
|
|
28
|
+
// only ever used as a monitor never runs a line of them.
|
|
29
|
+
const GAMES = Object.freeze({
|
|
30
|
+
tetrust: () => import('./tetrust.js').then((m) => m.renderTetrust),
|
|
31
|
+
blockout: () => import('./blockout.js').then((m) => m.renderBlockout),
|
|
32
|
+
blockanoid: () => import('./blockanoid.js').then((m) => m.renderBlockanoid),
|
|
33
|
+
scorched: () => import('./scorchedyard.js').then((m) => m.renderScorchedYard),
|
|
34
|
+
wolf3d: () => import('./wolf3d.js').then((m) => m.renderWolf3d),
|
|
35
|
+
doom: () => import('./doom.js').then((m) => m.renderDoom),
|
|
36
|
+
quake: () => import('./quake.js').then((m) => m.renderQuake),
|
|
37
|
+
});
|
|
38
|
+
const gameRenderers = new Map();
|
|
39
|
+
const gameLoads = new Map();
|
|
40
|
+
/** Draw a game's page, loading its module the first time; the page repaints once it has arrived. */
|
|
41
|
+
function renderGame(page, s) {
|
|
42
|
+
const draw = gameRenderers.get(page);
|
|
43
|
+
if (draw) { draw(s, state, helpers); return; }
|
|
44
|
+
if (gameLoads.has(page)) return;
|
|
45
|
+
gameLoads.set(page, GAMES[page]()
|
|
46
|
+
.then((fn) => { gameRenderers.set(page, fn); if (state.page === page) render(); })
|
|
47
|
+
.catch((e) => { gameLoads.delete(page); toast(`${page} could not be loaded: ${e.message}`, 'bad'); }));
|
|
48
|
+
}
|
|
22
49
|
import { renderAbout } from './about.js';
|
|
23
50
|
import { renderChain, renderMempool, renderPeers, renderNetwork, renderLogs, renderNode, renderAdmin, ensureLogsLoaded, init as initPanels, initChainDrill } from './panels.js';
|
|
24
51
|
|
|
52
|
+
followTheme();
|
|
53
|
+
|
|
25
54
|
// panels.js needs the formatters but must not import them from here (circular);
|
|
26
55
|
// they are injected once at module start instead.
|
|
27
56
|
initPanels(F);
|
|
@@ -288,8 +317,10 @@ function renderOverview(s) {
|
|
|
288
317
|
const stripOn = !!loadSettings().markets?.overviewSummary;
|
|
289
318
|
const strip = document.getElementById('ovMkSummary');
|
|
290
319
|
if (strip) {
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
// and nothing to show while the feed is off (the default): the strip stays hidden
|
|
321
|
+
const have = overviewMarkets && overviewMarkets.enabled !== false;
|
|
322
|
+
strip.hidden = !stripOn || !have;
|
|
323
|
+
if (stripOn && have) strip.innerHTML = marketsSummaryHtml(overviewMarkets, F);
|
|
293
324
|
}
|
|
294
325
|
|
|
295
326
|
const mp = s.mempool ?? {};
|
|
@@ -516,9 +547,9 @@ export function render() {
|
|
|
516
547
|
case 'explorer': renderExplorer(s, state, helpers); break;
|
|
517
548
|
case 'markets': renderMarkets(s, state, helpers); break;
|
|
518
549
|
case 'kiosk': renderKiosk(s, state, helpers); break;
|
|
519
|
-
case 'tetrust':
|
|
520
|
-
|
|
521
|
-
|
|
550
|
+
case 'tetrust': case 'blockout': case 'blockanoid': case 'scorched': case 'wolf3d': case 'doom': case 'quake':
|
|
551
|
+
renderGame(state.page, s);
|
|
552
|
+
break;
|
|
522
553
|
case 'about': renderAbout(s, state, helpers); break;
|
|
523
554
|
}
|
|
524
555
|
}
|
|
@@ -908,7 +939,7 @@ function shortBuild(build) {
|
|
|
908
939
|
// subroutes; they stay on the URL so every explorer page is a link.
|
|
909
940
|
// The games live behind the Diversions pop-down at the end of the nav; the menu shows as the
|
|
910
941
|
// active tab while one of them is open, since its own button is out of sight inside the popup.
|
|
911
|
-
const DIVERSION_PAGES = ['tetrust', 'blockout', 'blockanoid'];
|
|
942
|
+
const DIVERSION_PAGES = ['tetrust', 'blockout', 'blockanoid', 'scorched', 'wolf3d', 'doom', 'quake'];
|
|
912
943
|
|
|
913
944
|
function setPage(route) {
|
|
914
945
|
const [page, ...rest] = String(route).split('/');
|
|
@@ -1120,8 +1151,10 @@ async function boot() {
|
|
|
1120
1151
|
const drawSettings = () => {
|
|
1121
1152
|
const s = loadSettings();
|
|
1122
1153
|
if (!SETTINGS_PANEL.some((g) => g.group === cfgTab)) cfgTab = SETTINGS_PANEL[0].group;
|
|
1123
|
-
|
|
1124
|
-
|
|
1154
|
+
// one row per TAB_ROWS entry, labelled, the games last (operator: "Diversions go at the very end")
|
|
1155
|
+
const tab = (g) => `<button type="button" class="cfgtab${g.group === cfgTab ? ' on' : ''}" role="tab" aria-selected="${g.group === cfgTab}" data-cfgtab="${g.group}">${g.title}</button>`;
|
|
1156
|
+
const tabs = `<div class="cfgtabs" role="tablist">${TAB_ROWS.map((row) =>
|
|
1157
|
+
`<div class="cfgtabrow"><span class="cfgtablbl">${row.label}</span>${row.groups.map((id) => SETTINGS_PANEL.find((g) => g.group === id)).filter(Boolean).map(tab).join('')}</div>`).join('')}</div>`;
|
|
1125
1158
|
cfgBody.innerHTML = tabs + SETTINGS_PANEL.filter((g) => g.group === cfgTab).map((g) => {
|
|
1126
1159
|
// ALL / NONE, on the groups that ask for it (the two effects tabs): twenty-eight switches is
|
|
1127
1160
|
// a lot of clicking to answer "just show me the quiet board". This was "every row is a
|
|
@@ -1130,9 +1163,53 @@ async function boot() {
|
|
|
1130
1163
|
const bulk = g.bulk
|
|
1131
1164
|
? `<div class="cfgbulk"><button type="button" class="btn" data-cfgall="${g.group}">all on</button><button type="button" class="btn" data-cfgnone="${g.group}">all off</button></div>`
|
|
1132
1165
|
: '';
|
|
1133
|
-
|
|
1166
|
+
// THE SWITCH ROWS IN TWO COLUMNS, HINT AS THE TOOLTIP (2026-09-15, "same treatment on the
|
|
1167
|
+
// settings panel"): on the two effects tabs, thirty-odd switches with a line of hint each ran
|
|
1168
|
+
// to 2,600px of scrolling; as compact rows two across, with the hint on hover, the whole tab
|
|
1169
|
+
// is a screen. The sliders above them keep their hints in view.
|
|
1170
|
+
const compact = !!g.bulk;
|
|
1171
|
+
let open = false;
|
|
1172
|
+
const rowsHtml = g.rows.map((r) => {
|
|
1134
1173
|
const v = s[g.group][r.key];
|
|
1135
1174
|
const id = `cfg-${g.group}-${r.key}`;
|
|
1175
|
+
// THE APPEARANCE TAB'S OWN CONTROLS: the mode as a segmented row of buttons, the theme as cards
|
|
1176
|
+
// with a swatch strip and a line about each (the shape of every editor's appearance page),
|
|
1177
|
+
// and a "start Custom from this" button under the cards. The swatches' colours are set after
|
|
1178
|
+
// the markup lands (data-swatch), the one way the CSP lets a script colour an element.
|
|
1179
|
+
if (r.kind === 'segment') {
|
|
1180
|
+
const seg = `<span class="cfgseg" role="radiogroup" aria-label="${r.label}">${r.options.map(([val, label]) =>
|
|
1181
|
+
`<button type="button" class="cfgsegbtn${val === v ? ' on' : ''}" role="radio" aria-checked="${val === v}" data-cfgset="${g.group}.${r.key}" data-value="${val}">${label}</button>`).join('')}</span>`;
|
|
1182
|
+
return `<div class="cfgrow"><b><label>${r.label}</label></b><span>${seg}</span><i>${r.hint}</i></div>`;
|
|
1183
|
+
}
|
|
1184
|
+
// WHICH SKY, WHERE (docs/PLAN-SKIES.md): the table at the top of the Sky tab. One row per board
|
|
1185
|
+
// that has a sky, each a select on that board's own `sky` key -- the same setting the board's
|
|
1186
|
+
// tab shows, through the same data-cfg path, so a change in either place is one change.
|
|
1187
|
+
if (r.kind === 'skymap') {
|
|
1188
|
+
const rows = SKY_BOARDS.map((b) => {
|
|
1189
|
+
const cur = s[b.group]?.sky ?? 'galaxy';
|
|
1190
|
+
const sel = `<select data-cfg="${b.group}.sky" aria-label="${b.label}: sky">${SKY_CHOICES.map(([val, label]) => `<option value="${val}"${val === cur ? ' selected' : ''}>${label}</option>`).join('')}</select>`;
|
|
1191
|
+
return `<tr><th scope="row">${b.label}${b.note ? `<small>${b.note}</small>` : ''}</th><td>${sel}</td></tr>`;
|
|
1192
|
+
}).join('');
|
|
1193
|
+
return `<div class="cfgrow cfgskymap"><b><label>${r.label}</label></b><span><table class="skymap">${rows}</table></span><i>${r.hint}</i></div>`;
|
|
1194
|
+
}
|
|
1195
|
+
// a heading inside a group: the two skies of the Sky tab, each introducing its own rows
|
|
1196
|
+
if (r.kind === 'heading') {
|
|
1197
|
+
const close = open ? '</div>' : '';
|
|
1198
|
+
open = false;
|
|
1199
|
+
return `${close}<div class="cfghead"><b>${r.label}</b><i>${r.hint}</i></div>`;
|
|
1200
|
+
}
|
|
1201
|
+
if (r.kind === 'cards') {
|
|
1202
|
+
const scheme = resolveScheme(s.appearance);
|
|
1203
|
+
const cards = r.options.map(([val, label]) => {
|
|
1204
|
+
const face = val === 'custom' ? resolveTheme(s) : presetFace(val, scheme);
|
|
1205
|
+
const blurb = val === 'custom' ? 'Your own nine colours, below' : PRESETS[val].blurb;
|
|
1206
|
+
const sw = ['bg', 'panel', 'text', 'accent', 'ok', 'warn', 'bad'].map((k) => `<i data-swatch="${face[k]}" title="${k}"></i>`).join('');
|
|
1207
|
+
return `<button type="button" class="thcard${val === v ? ' on' : ''}" role="radio" aria-checked="${val === v}" data-cfgset="${g.group}.${r.key}" data-value="${val}" data-face-bg="${face.bg}" data-face-fg="${face.text}" data-face-line="${face.line}">`
|
|
1208
|
+
+ `<span class="thsw">${sw}</span><b>${label}</b><small>${blurb}</small></button>`;
|
|
1209
|
+
}).join('');
|
|
1210
|
+
const from = v === 'custom' ? '' : `<button type="button" class="btn cfgcustomise" data-cfgcustomise="1" title="Copy this theme's colours into the nine pickers below and switch to Custom">customise ${PRESETS[v]?.label ?? 'this'} →</button>`;
|
|
1211
|
+
return `<div class="cfgrow cfgcards"><b><label>${r.label}</label></b><span>${from}</span><span class="thcards" role="radiogroup" aria-label="${r.label}">${cards}</span><i>${r.hint}</i></div>`;
|
|
1212
|
+
}
|
|
1136
1213
|
const ctl = r.kind === 'toggle'
|
|
1137
1214
|
? `<input type="checkbox" id="${id}" data-cfg="${g.group}.${r.key}"${v ? ' checked' : ''}>`
|
|
1138
1215
|
: r.kind === 'choice'
|
|
@@ -1140,9 +1217,32 @@ async function boot() {
|
|
|
1140
1217
|
: r.kind === 'colour'
|
|
1141
1218
|
? `<input type="color" id="${id}" data-cfg="${g.group}.${r.key}" value="${v}">`
|
|
1142
1219
|
: `<span class="cfgrange"><input type="range" id="${id}" data-cfg="${g.group}.${r.key}" min="${r.min}" max="${r.max}" step="${r.step}" value="${v}"><span class="val" data-val-for="${g.group}.${r.key}">${formatRangeValue(r.step, v)}</span></span>`;
|
|
1143
|
-
|
|
1144
|
-
|
|
1220
|
+
// (and the nine custom colours of the Appearance tab the same way: two across, hint as tooltip,
|
|
1221
|
+
// dimmed while another theme is chosen, so the tab is a screen)
|
|
1222
|
+
// A CONTROL THAT DOES NOTHING RIGHT NOW SAYS SO (operator, 2026-09-16: "It's counter
|
|
1223
|
+
// intuitive in the preferences to display a spiral galaxy for a night/day scene"). A row
|
|
1224
|
+
// whose `dimWhen` is true of the current settings is dimmed and says which sky it belongs
|
|
1225
|
+
// to, rather than being hidden: the setting is still there, and still remembered.
|
|
1226
|
+
const off = (r.custom && s.appearance.theme !== 'custom') || (typeof r.dimWhen === 'function' && r.dimWhen(s));
|
|
1227
|
+
const idle = off ? ' idle' : '';
|
|
1228
|
+
const note = off && r.dimNote ? ` <em>${r.dimNote}</em>` : '';
|
|
1229
|
+
if ((compact && r.kind === 'toggle') || r.custom) {
|
|
1230
|
+
const start = open ? '' : '<div class="cfgrows2">';
|
|
1231
|
+
open = true;
|
|
1232
|
+
return `${start}<div class="cfgrow compact${idle}" title="${String(off && r.dimNote ? `${r.dimNote} — ${r.hint}` : r.hint).replace(/"/g, '"')}"><b><label for="${id}">${r.label}</label></b><span>${ctl}</span></div>`;
|
|
1233
|
+
}
|
|
1234
|
+
const close = open ? '</div>' : '';
|
|
1235
|
+
open = false;
|
|
1236
|
+
return `${close}<div class="cfgrow${idle}"><b><label for="${id}">${r.label}</label></b><span>${ctl}</span><i>${r.hint}${note}</i></div>`;
|
|
1237
|
+
}).join('') + (open ? '</div>' : '');
|
|
1238
|
+
// the Space effects tab says so while the Living sky has them off (settings.js spaceOptions)
|
|
1239
|
+
const held = g.group === 'effects' && s.sky.type === 'living' ? '<p class="cfgheld">Off while the sky is the Living sky (Sky tab): these play over space. Your switches are kept.</p>' : '';
|
|
1240
|
+
return `<div class="cfggroup"><h3>${g.title}</h3><p>${g.note}</p>${held}${bulk}${rowsHtml}</div>`;
|
|
1145
1241
|
}).join('');
|
|
1242
|
+
for (const el of cfgBody.querySelectorAll('[data-swatch]')) el.style.setProperty('background', el.dataset.swatch);
|
|
1243
|
+
for (const el of cfgBody.querySelectorAll('.thcard')) {
|
|
1244
|
+
el.style.setProperty('--th-bg', el.dataset.faceBg); el.style.setProperty('--th-fg', el.dataset.faceFg); el.style.setProperty('--th-line', el.dataset.faceLine);
|
|
1245
|
+
}
|
|
1146
1246
|
};
|
|
1147
1247
|
const openSettings = (open) => {
|
|
1148
1248
|
cfgWrap.classList.toggle('hidden', !open);
|
|
@@ -1163,6 +1263,21 @@ async function boot() {
|
|
|
1163
1263
|
cfgBody.addEventListener('click', (e) => {
|
|
1164
1264
|
const tab = e.target.closest?.('[data-cfgtab]');
|
|
1165
1265
|
if (tab) { cfgTab = tab.dataset.cfgtab; drawSettings(); return; }
|
|
1266
|
+
// a segment button or a theme card: one value, then the tab redraws so the cards' swatches
|
|
1267
|
+
// show the face the new mode picks
|
|
1268
|
+
const pick = e.target.closest?.('[data-cfgset]');
|
|
1269
|
+
if (pick) { setSetting(loadSettings(), pick.dataset.cfgset, pick.dataset.value); drawSettings(); repaintSoon(); return; }
|
|
1270
|
+
// "customise this": the nine base colours of the theme on screen become the Custom pickers'
|
|
1271
|
+
// values, and Custom is selected, so the pickers start from a look rather than from the shipped one
|
|
1272
|
+
if (e.target.closest?.('[data-cfgcustomise]')) {
|
|
1273
|
+
const face = resolveTheme(loadSettings());
|
|
1274
|
+
let next = loadSettings();
|
|
1275
|
+
const keyOf = { bg: 'customBg', panel: 'customPanel', text: 'customText', muted: 'customMuted', accent: 'customAccent', line: 'customLine', ok: 'customOk', warn: 'customWarn', bad: 'customBad' };
|
|
1276
|
+
for (const k of BASE_KEYS) next = setSetting(next, `appearance.${keyOf[k]}`, face[k]);
|
|
1277
|
+
setSetting(next, 'appearance.theme', 'custom');
|
|
1278
|
+
drawSettings(); repaintSoon();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1166
1281
|
const bulk = e.target.closest?.('[data-cfgall], [data-cfgnone]');
|
|
1167
1282
|
if (!bulk) return;
|
|
1168
1283
|
const on = bulk.hasAttribute('data-cfgall');
|