blockyard 0.1.0 → 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 +108 -0
- package/README.md +13 -11
- package/SECURITY.md +2 -2
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +36 -5
- package/docs/CONFIGURATION.md +6 -4
- package/docs/DEFECTS.md +4 -1
- package/docs/GETTING-STARTED.md +14 -7
- package/docs/INSTALL.md +7 -4
- 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 +26 -7
- package/docs/TROUBLESHOOTING.md +10 -5
- package/docs/USER-GUIDE.md +239 -9
- package/package.json +4 -2
- package/public/css/app.css +87 -0
- package/public/index.html +58 -6
- package/public/js/app.js +60 -19
- package/public/js/blockanoid.js +15 -7
- package/public/js/blockout.js +15 -7
- package/public/js/blockscene3d.js +51 -11
- package/public/js/depthchart.js +1 -1
- package/public/js/details3d.js +25 -2
- package/public/js/explorer.js +7 -1
- package/public/js/livingsky.js +494 -0
- package/public/js/login.js +3 -2
- package/public/js/mining.js +4 -4
- package/public/js/panels.js +27 -18
- 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 +266 -80
- package/public/js/tetrust.js +15 -6
- package/public/js/tetsound.js +35 -5
- package/scripts/check.js +46 -0
- package/scripts/index-build.js +9 -2
- package/scripts/pool-map.js +152 -36
- package/scripts/setup.js +108 -10
- package/scripts/shots.mjs +21 -0
- package/scripts/smoke.sh +6 -5
- 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 +432 -56
- 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 +24 -11
- package/server/collect/network.js +19 -9
- package/server/config.js +7 -0
- package/server/http/api.js +70 -13
- package/server/http/server.js +22 -5
- package/server/http/sse.js +53 -7
- package/server/main.js +13 -3
- 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/systemd/blockyard.service +34 -3
package/public/js/app.js
CHANGED
|
@@ -11,8 +11,7 @@ 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
|
-
} from './settings.js';
|
|
14
|
+
SETTINGS_KEY, PANEL as SETTINGS_PANEL, formatRangeValue, SKY_BOARDS, SKY_CHOICES } from './settings.js';
|
|
16
15
|
// APPEARANCE (2026-09-16): the theme goes on <html> before the first paint, from the browser's copy
|
|
17
16
|
// of the settings, and follows every change after (followTheme); the cards' swatches are painted
|
|
18
17
|
// through the CSSOM because the CSP refuses a style attribute in markup
|
|
@@ -21,12 +20,32 @@ import { followTheme, presetFace, resolveScheme, resolveTheme, PRESETS, BASE_KEY
|
|
|
21
20
|
import { renderExplorer } from './explorer.js';
|
|
22
21
|
import { renderMarkets, summaryHtml as marketsSummaryHtml, REFRESH_MS as MARKETS_REFRESH_MS } from './markets.js';
|
|
23
22
|
import { renderKiosk } from './kiosk.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import
|
|
29
|
-
|
|
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
|
+
}
|
|
30
49
|
import { renderAbout } from './about.js';
|
|
31
50
|
import { renderChain, renderMempool, renderPeers, renderNetwork, renderLogs, renderNode, renderAdmin, ensureLogsLoaded, init as initPanels, initChainDrill } from './panels.js';
|
|
32
51
|
|
|
@@ -528,12 +547,9 @@ export function render() {
|
|
|
528
547
|
case 'explorer': renderExplorer(s, state, helpers); break;
|
|
529
548
|
case 'markets': renderMarkets(s, state, helpers); break;
|
|
530
549
|
case 'kiosk': renderKiosk(s, state, helpers); break;
|
|
531
|
-
case 'tetrust':
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
case 'wolf3d': renderWolf3d(s, state, helpers); break;
|
|
535
|
-
case 'doom': renderDoom(s, state, helpers); break;
|
|
536
|
-
case 'quake': renderQuake(s, state, helpers); break;
|
|
550
|
+
case 'tetrust': case 'blockout': case 'blockanoid': case 'scorched': case 'wolf3d': case 'doom': case 'quake':
|
|
551
|
+
renderGame(state.page, s);
|
|
552
|
+
break;
|
|
537
553
|
case 'about': renderAbout(s, state, helpers); break;
|
|
538
554
|
}
|
|
539
555
|
}
|
|
@@ -923,7 +939,7 @@ function shortBuild(build) {
|
|
|
923
939
|
// subroutes; they stay on the URL so every explorer page is a link.
|
|
924
940
|
// The games live behind the Diversions pop-down at the end of the nav; the menu shows as the
|
|
925
941
|
// active tab while one of them is open, since its own button is out of sight inside the popup.
|
|
926
|
-
const DIVERSION_PAGES = ['tetrust', 'blockout', 'blockanoid', 'wolf3d', 'doom', 'quake'];
|
|
942
|
+
const DIVERSION_PAGES = ['tetrust', 'blockout', 'blockanoid', 'scorched', 'wolf3d', 'doom', 'quake'];
|
|
927
943
|
|
|
928
944
|
function setPage(route) {
|
|
929
945
|
const [page, ...rest] = String(route).split('/');
|
|
@@ -1165,6 +1181,23 @@ async function boot() {
|
|
|
1165
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>`;
|
|
1166
1182
|
return `<div class="cfgrow"><b><label>${r.label}</label></b><span>${seg}</span><i>${r.hint}</i></div>`;
|
|
1167
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
|
+
}
|
|
1168
1201
|
if (r.kind === 'cards') {
|
|
1169
1202
|
const scheme = resolveScheme(s.appearance);
|
|
1170
1203
|
const cards = r.options.map(([val, label]) => {
|
|
@@ -1186,17 +1219,25 @@ async function boot() {
|
|
|
1186
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>`;
|
|
1187
1220
|
// (and the nine custom colours of the Appearance tab the same way: two across, hint as tooltip,
|
|
1188
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>` : '';
|
|
1189
1229
|
if ((compact && r.kind === 'toggle') || r.custom) {
|
|
1190
1230
|
const start = open ? '' : '<div class="cfgrows2">';
|
|
1191
1231
|
open = true;
|
|
1192
|
-
|
|
1193
|
-
return `${start}<div class="cfgrow compact${idle}" title="${String(r.hint).replace(/"/g, '"')}"><b><label for="${id}">${r.label}</label></b><span>${ctl}</span></div>`;
|
|
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>`;
|
|
1194
1233
|
}
|
|
1195
1234
|
const close = open ? '</div>' : '';
|
|
1196
1235
|
open = false;
|
|
1197
|
-
return `${close}<div class="cfgrow"><b><label for="${id}">${r.label}</label></b><span>${ctl}</span><i>${r.hint}</i></div>`;
|
|
1236
|
+
return `${close}<div class="cfgrow${idle}"><b><label for="${id}">${r.label}</label></b><span>${ctl}</span><i>${r.hint}${note}</i></div>`;
|
|
1198
1237
|
}).join('') + (open ? '</div>' : '');
|
|
1199
|
-
|
|
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>`;
|
|
1200
1241
|
}).join('');
|
|
1201
1242
|
for (const el of cfgBody.querySelectorAll('[data-swatch]')) el.style.setProperty('background', el.dataset.swatch);
|
|
1202
1243
|
for (const el of cfgBody.querySelectorAll('.thcard')) {
|
package/public/js/blockanoid.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
newGame, advance, step, movePaddle, nudge, launch, fire, tiles, remaining, powers, setOptions,
|
|
11
11
|
COLS, ROWS, LIVES, CAPSULE_LETTER, layoutFor,
|
|
12
12
|
} from './arkanoid.js';
|
|
13
|
-
import { loadSettings, setSetting, blockanoidOptions } from './settings.js';
|
|
13
|
+
import { loadSettings, setSetting, blockanoidOptions, nextSky, SKY_LABELS } from './settings.js';
|
|
14
14
|
import * as sound from './tetsound.js';
|
|
15
15
|
|
|
16
16
|
const SCORES_KEY = 'blockyard.blockanoid.scores';
|
|
@@ -130,9 +130,20 @@ function drawScores(highlightAt = null) {
|
|
|
130
130
|
if (t.innerHTML !== html) t.innerHTML = html;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const SWITCHES = [['
|
|
133
|
+
const SWITCHES = [['baNeon', 'neon'], ['baSfx', 'sfx']];
|
|
134
|
+
/** The sky button says which sky it is on, and lights up while there is one. */
|
|
135
|
+
function skyButton(id, sky) {
|
|
136
|
+
const b = el(id);
|
|
137
|
+
if (!b) return;
|
|
138
|
+
b.textContent = `\u2600 ${SKY_LABELS[sky] ?? sky}`;
|
|
139
|
+
b.classList.toggle('on', sky !== 'none');
|
|
140
|
+
b.setAttribute('aria-pressed', sky !== 'none' ? 'true' : 'false');
|
|
141
|
+
}
|
|
134
142
|
function drawSwitches() {
|
|
135
143
|
const b = blockanoidOptions(loadSettings());
|
|
144
|
+
// ONE SKY BUTTON (docs/PLAN-SKIES.md): Galaxy, Earth or none, round the ring, in place of the old
|
|
145
|
+
// star field and galaxy pair -- the same setting the Sky tab's table shows for this board
|
|
146
|
+
skyButton('baSkySw', b.sky.sky);
|
|
136
147
|
for (const [id, key] of SWITCHES) {
|
|
137
148
|
const btn = el(id);
|
|
138
149
|
if (!btn) continue;
|
|
@@ -146,7 +157,6 @@ function flip(key) {
|
|
|
146
157
|
setSetting(loadSettings(), `blockanoid.${key}`, !cur);
|
|
147
158
|
sound.unlock();
|
|
148
159
|
drawSwitches();
|
|
149
|
-
if (key === 'stars' || key === 'galaxy') drawSky();
|
|
150
160
|
// capsules and minions change the RULES, so a flip must reach the game IN PLAY, not just the
|
|
151
161
|
// next one -- otherwise the switch does nothing until you lose, which reads as a broken control
|
|
152
162
|
if (G.game) { const b = blockanoidOptions(loadSettings()); setOptions(G.game, { capsules: b.capsules, enemies: b.enemies }); }
|
|
@@ -159,10 +169,7 @@ function drawSky() {
|
|
|
159
169
|
const b = blockanoidOptions(loadSettings());
|
|
160
170
|
board3d(sky, [], {
|
|
161
171
|
...SKY,
|
|
162
|
-
|
|
163
|
-
starDensity: b.starDensity, starBrightness: b.starBrightness,
|
|
164
|
-
nebulae: b.nebulae, galaxies: b.galaxies, dust: b.dust, clusters: b.clusters,
|
|
165
|
-
starColours: b.starColours, starGlints: b.starGlints,
|
|
172
|
+
...b.sky, // which sky this board chose, and what it is made of (settings.js skyFor)
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
175
|
|
|
@@ -327,6 +334,7 @@ function bind() {
|
|
|
327
334
|
});
|
|
328
335
|
court?.addEventListener('contextmenu', (e) => { if (G.running) e.preventDefault(); });
|
|
329
336
|
for (const [id, key] of SWITCHES) el(id)?.addEventListener('click', () => flip(key));
|
|
337
|
+
el('baSkySw')?.addEventListener('click', () => { const s = loadSettings(); setSetting(s, 'blockanoid.sky', nextSky(blockanoidOptions(s).sky.sky)); sound.unlock(); drawSwitches(); drawSky(); draw(); });
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
export function renderBlockanoid(s, state, h) {
|
package/public/js/blockout.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
newGame, advance, step, movePaddle, nudge, launch, tiles, remaining,
|
|
13
13
|
COLS, ROWS, LIVES,
|
|
14
14
|
} from './breakout.js';
|
|
15
|
-
import { loadSettings, setSetting, blockoutOptions } from './settings.js';
|
|
15
|
+
import { loadSettings, setSetting, blockoutOptions, nextSky, SKY_LABELS } from './settings.js';
|
|
16
16
|
import * as sound from './tetsound.js';
|
|
17
17
|
|
|
18
18
|
const SCORES_KEY = 'blockyard.blockout.scores';
|
|
@@ -145,9 +145,20 @@ function drawScores(highlightAt = null) {
|
|
|
145
145
|
if (t.innerHTML !== html) t.innerHTML = html;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
const SWITCHES = [['
|
|
148
|
+
const SWITCHES = [['boNeon', 'neon'], ['boSfx', 'sfx']];
|
|
149
|
+
/** The sky button says which sky it is on, and lights up while there is one. */
|
|
150
|
+
function skyButton(id, sky) {
|
|
151
|
+
const b = el(id);
|
|
152
|
+
if (!b) return;
|
|
153
|
+
b.textContent = `\u2600 ${SKY_LABELS[sky] ?? sky}`;
|
|
154
|
+
b.classList.toggle('on', sky !== 'none');
|
|
155
|
+
b.setAttribute('aria-pressed', sky !== 'none' ? 'true' : 'false');
|
|
156
|
+
}
|
|
149
157
|
function drawSwitches() {
|
|
150
158
|
const b = blockoutOptions(loadSettings());
|
|
159
|
+
// ONE SKY BUTTON (docs/PLAN-SKIES.md): Galaxy, Earth or none, round the ring, in place of the old
|
|
160
|
+
// star field and galaxy pair -- the same setting the Sky tab's table shows for this board
|
|
161
|
+
skyButton('boSkySw', b.sky.sky);
|
|
151
162
|
for (const [id, key] of SWITCHES) {
|
|
152
163
|
const btn = el(id);
|
|
153
164
|
if (!btn) continue;
|
|
@@ -161,7 +172,6 @@ function flip(key) {
|
|
|
161
172
|
setSetting(loadSettings(), `blockout.${key}`, !cur);
|
|
162
173
|
sound.unlock();
|
|
163
174
|
drawSwitches();
|
|
164
|
-
if (key === 'stars' || key === 'galaxy') drawSky();
|
|
165
175
|
G.dirty = true;
|
|
166
176
|
draw();
|
|
167
177
|
}
|
|
@@ -172,10 +182,7 @@ function drawSky() {
|
|
|
172
182
|
const b = blockoutOptions(loadSettings());
|
|
173
183
|
board3d(sky, [], {
|
|
174
184
|
...SKY,
|
|
175
|
-
|
|
176
|
-
starDensity: b.starDensity, starBrightness: b.starBrightness,
|
|
177
|
-
nebulae: b.nebulae, galaxies: b.galaxies, dust: b.dust, clusters: b.clusters,
|
|
178
|
-
starColours: b.starColours, starGlints: b.starGlints,
|
|
185
|
+
...b.sky, // which sky this board chose, and what it is made of (settings.js skyFor)
|
|
179
186
|
});
|
|
180
187
|
}
|
|
181
188
|
|
|
@@ -329,6 +336,7 @@ function bind() {
|
|
|
329
336
|
court?.addEventListener('pointermove', onPointer);
|
|
330
337
|
court?.addEventListener('pointerdown', (e) => { onPointer(e); serve(); });
|
|
331
338
|
for (const [id, key] of SWITCHES) el(id)?.addEventListener('click', () => flip(key));
|
|
339
|
+
el('boSkySw')?.addEventListener('click', () => { const s = loadSettings(); setSetting(s, 'blockout.sky', nextSky(blockoutOptions(s).sky.sky)); sound.unlock(); drawSwitches(); drawSky(); draw(); });
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
export function renderBlockout(s, state, h) {
|
|
@@ -852,21 +852,50 @@ const flipOf = (o) => (o.flipY === false ? -1 : 1);
|
|
|
852
852
|
// settings"). Each lamp is a direction over the board for the dome's slope shading (grid x,
|
|
853
853
|
// screen-up rows, up) and a direction across the screen for the side faces (x right, y down);
|
|
854
854
|
// `overhead` is straight above, so no slope is in shade and every side takes the same light.
|
|
855
|
+
// SIX PLACEMENTS AT THREE HEIGHTS (operator, 2026-09-16: "add more camera light angles ... Low
|
|
856
|
+
// Middle and High light placement locations, top left, top right, bottom left, bottom right,
|
|
857
|
+
// viewer, direct overhead"). A placement is a direction across the board (x right, y screen-up);
|
|
858
|
+
// the height is the lamp's elevation over it: low 25°, middle 45°, high 65°. `overhead` ignores
|
|
859
|
+
// the height. The old names (upper-left, upper-right) still answer, as top-left and top-right at
|
|
860
|
+
// the middle height; `front` is the lamp at the viewer. (Not 'viewer': `light: 'viewer'` is the
|
|
861
|
+
// Markets board's own flat, viewer-lit finish -- viewerLit below -- and a placement of that name
|
|
862
|
+
// turned the whole block board flat-shaded, which the operator saw at once on 2026-09-16.)
|
|
863
|
+
export const LIGHT_PLACES = Object.freeze({
|
|
864
|
+
'overhead': [0, 0], 'top-left': [-1, 1], 'top-right': [1, 1], 'bottom-left': [-1, -1], 'bottom-right': [1, -1], 'front': [0, -1],
|
|
865
|
+
});
|
|
866
|
+
export const LIGHT_HEIGHTS = Object.freeze({ low: 25, middle: 45, high: 65 });
|
|
867
|
+
const LIGHT_ALIASES = Object.freeze({ 'upper-left': 'top-left', 'upper-right': 'top-right' });
|
|
868
|
+
export function lampFor(place, height = 'middle') {
|
|
869
|
+
const p = LIGHT_PLACES[place] ?? LIGHT_PLACES['top-left'];
|
|
870
|
+
if (!p[0] && !p[1]) return { L: [0, 0, 1], side: [0, 0] };
|
|
871
|
+
const el = ((LIGHT_HEIGHTS[height] ?? 45) * Math.PI) / 180;
|
|
872
|
+
const n = Math.hypot(p[0], p[1]) || 1;
|
|
873
|
+
const ax = p[0] / n, ay = p[1] / n;
|
|
874
|
+
// the side faces take the lamp's direction across the screen (y down), weaker the higher it hangs
|
|
875
|
+
const k = 0.9 * Math.cos(el) / Math.cos((45 * Math.PI) / 180);
|
|
876
|
+
return { L: [ax * Math.cos(el), ay * Math.cos(el), Math.sin(el)], side: [ax * k, -ay * k] };
|
|
877
|
+
}
|
|
855
878
|
export const LIGHTS = Object.freeze({
|
|
856
|
-
'overhead':
|
|
857
|
-
'
|
|
858
|
-
'upper-
|
|
859
|
-
'front': { L: [0, -0.55, 0.63], side: [0, 0.9] },
|
|
879
|
+
'overhead': lampFor('overhead'),
|
|
880
|
+
'top-left': lampFor('top-left'), 'top-right': lampFor('top-right'), 'bottom-left': lampFor('bottom-left'), 'bottom-right': lampFor('bottom-right'), 'front': lampFor('front'),
|
|
881
|
+
'upper-left': lampFor('top-left'), 'upper-right': lampFor('top-right'),
|
|
860
882
|
});
|
|
861
|
-
export const LIGHT_DEFAULT = '
|
|
883
|
+
export const LIGHT_DEFAULT = 'top-left';
|
|
884
|
+
// the middle of the shipped brightness range: `lightGain` stretches the faces about this point
|
|
885
|
+
const LIT_MID = 0.58;
|
|
862
886
|
const NEON_HEX = /^#[0-9a-f]{6}$/i;
|
|
863
887
|
export function lightOf(o = {}) {
|
|
864
|
-
|
|
888
|
+
const name = LIGHT_ALIASES[o.light] ?? o.light;
|
|
889
|
+
return LIGHT_PLACES[name] ? name : o.overheadLight === true ? 'overhead' : LIGHT_DEFAULT;
|
|
890
|
+
}
|
|
891
|
+
/** The lamp a board asks for: its placement at its height. */
|
|
892
|
+
export function lampOf(o = {}) {
|
|
893
|
+
return lampFor(lightOf(o), LIGHT_HEIGHTS[o.lightHeight] ? o.lightHeight : 'middle');
|
|
865
894
|
}
|
|
866
895
|
export function domeLight(t, o = {}) {
|
|
867
896
|
const { dome = 0, gridW = 0, gridH = 0 } = o;
|
|
868
897
|
if (!dome || !gridW || !gridH) return 1;
|
|
869
|
-
const lamp =
|
|
898
|
+
const lamp = lampOf(o);
|
|
870
899
|
// straight above: the slope shades nothing -- the bottom rows, which lean away from a corner
|
|
871
900
|
// lamp and sat at the 0.6 floor, read as bright as the middle
|
|
872
901
|
if (lamp.L[0] === 0 && lamp.L[1] === 0) return 1;
|
|
@@ -1271,7 +1300,7 @@ export function buildScene(tiles, o = {}) {
|
|
|
1271
1300
|
// among equals the one further from the vanishing point first (its sides
|
|
1272
1301
|
// lean outward, away from its nearer neighbours).
|
|
1273
1302
|
const vx0 = o.vanishX ?? 0, vy0 = o.vanishY ?? 0;
|
|
1274
|
-
const ordered = o.oblique && o.order === 'diagonal' ? diagonalOrder(tiles) : o.oblique ? obliqueOrder(tiles, o) : tiles.map((t) => (growth.has(t) ? { ...t, boost: growth.get(t) } : t))
|
|
1303
|
+
const ordered = o.order === 'given' ? tiles.slice() : o.oblique && o.order === 'diagonal' ? diagonalOrder(tiles) : o.oblique ? obliqueOrder(tiles, o) : tiles.map((t) => (growth.has(t) ? { ...t, boost: growth.get(t) } : t))
|
|
1275
1304
|
.map((t) => { const c = project(t.x + t.s / 2, t.y + t.s / 2, 0, o); return { t, top: (t.z ?? 0) + (t.floor ?? 0) + cubeHeight(t), d: Math.hypot(c.x - vx0, c.y - vy0) }; })
|
|
1276
1305
|
// Under the oblique camera a cube reaches STRAIGHT UP from its footprint (measured: height
|
|
1277
1306
|
// moves screen y only, grid x moves screen x only), so it can only cover blocks at smaller
|
|
@@ -1326,7 +1355,10 @@ export function buildScene(tiles, o = {}) {
|
|
|
1326
1355
|
})).filter((e) => e.k > 0.01) : [];
|
|
1327
1356
|
// the lamp's direction across the screen, for the side faces (LIGHTS); under the overhead
|
|
1328
1357
|
// lamp every side takes the same light, a shade under the top
|
|
1329
|
-
const lampSide =
|
|
1358
|
+
const lampSide = lampOf(o).side;
|
|
1359
|
+
// the lamp's hardness and how bright the tops sit under it (see the side loop below)
|
|
1360
|
+
const gain = Number.isFinite(o.lightGain) ? Math.max(0.2, Math.min(3, o.lightGain)) : 1;
|
|
1361
|
+
const topLit = Number.isFinite(o.topLight) ? Math.max(0.1, Math.min(1.4, o.topLight)) : 0.8;
|
|
1330
1362
|
for (const t of ordered) {
|
|
1331
1363
|
const fxv = (t.z ?? 0) > 0.02 ? FX_NONE : fxAt(t, o.fx);
|
|
1332
1364
|
// GONE FOR THE DURATION, not deleted: a hidden cube is skipped this frame and drawn again the
|
|
@@ -1517,11 +1549,19 @@ export function buildScene(tiles, o = {}) {
|
|
|
1517
1549
|
// brighter, one turned away falls into shadow
|
|
1518
1550
|
const d = side.nx * lampSide[0] + side.ny * lampSide[1];
|
|
1519
1551
|
// from the viewer: a face pointing down the screen (toward the camera) takes the most light
|
|
1520
|
-
const
|
|
1552
|
+
const k0 = viewerLit ? 0.6 + 0.42 * Math.max(0, side.ny) + 0.3 * Math.max(0, side.nx) - 0.16 * Math.max(0, -side.nx) : 0.34 + 0.26 * (d + 1);
|
|
1553
|
+
// HOW HARD THE LAMP IS (operator, 2026-09-16: "we need better lighting on the front of the
|
|
1554
|
+
// blocks. Still looks too washed out and not illuminated well enough"). The shipped range put
|
|
1555
|
+
// every face between 0.34 and 0.86 of its colour with the top at 0.8: a narrow band, and a
|
|
1556
|
+
// board of it reads as tinted rather than lit. `lightGain` stretches that range about its
|
|
1557
|
+
// middle -- above 1 the face turned to the lamp goes brighter than its own colour and the one
|
|
1558
|
+
// turned away falls properly dark. 1 is exactly what every board drew before this.
|
|
1559
|
+
const k = Math.max(0.06, Math.min(1.4, LIT_MID + (k0 - LIT_MID) * gain));
|
|
1521
1560
|
out.push({ txid: t.txid, face: 'side', key: side.key, points: side.points, fill: shade(c, k * lit, a), ...(viewerLit ? { stroke: lift(c, 0.45, round3(0.5 * a)) } : {}) });
|
|
1522
1561
|
side.points.forEach(note);
|
|
1523
1562
|
}
|
|
1524
|
-
|
|
1563
|
+
const topK = viewerLit ? 1.02 : Math.max(0.06, Math.min(1.4, LIT_MID + (topLit - LIT_MID) * gain));
|
|
1564
|
+
out.push({ txid: t.txid, face: 'top', points: f.top, fill: shade(c, topK * lit, a), stroke: viewerLit ? lift(c, 0.55, round3(0.6 * a)) : `rgba(0,0,0,${round3(seam * a)})` });
|
|
1525
1565
|
f.top.forEach(note);
|
|
1526
1566
|
|
|
1527
1567
|
if (f.innerG && !viewerLit) {
|
package/public/js/depthchart.js
CHANGED
|
@@ -242,7 +242,7 @@ const offText = (d) => (d?.enabled === false ? (d.polling === false ? 'market po
|
|
|
242
242
|
|
|
243
243
|
export function depthNote(d, fmt) {
|
|
244
244
|
if (!d) return 'asking the exchanges for their order books…';
|
|
245
|
-
if (d.enabled === false) return d.note
|
|
245
|
+
if (d.enabled === false) return d.note != null ? fmt.esc(d.note) : 'market data is off on this monitor'; // markup sink: escaped (audit 2026-09-16, I2)
|
|
246
246
|
if (d.warming) return 'reading the order books for the first time…';
|
|
247
247
|
const agoL = AGOS.find(([s]) => s === d.ago)?.[1] ?? `${d.ago} s`;
|
|
248
248
|
const parts = [`books read at ${clock(d.at)} UTC, every ${Math.round((d.bookMs ?? DEPTH_MS) / 1000)} s while this tab is open`];
|
package/public/js/details3d.js
CHANGED
|
@@ -19,6 +19,7 @@ import { planTransition, frameAt, fitToBox, project, fxFront, TRANSITION, SLAB_H
|
|
|
19
19
|
// This module keeps three seams and nothing else -- build here in startFx, frame in fxNow,
|
|
20
20
|
// draw in paintFrame -- so fifty agents do not become fifty `if`s in the renderer.
|
|
21
21
|
import { AGENTS, isAgent, rng, lensFlare, saucerAbove } from './agents.js';
|
|
22
|
+
import { drawLivingSky } from './livingsky.js';
|
|
22
23
|
|
|
23
24
|
const STATE = new WeakMap();
|
|
24
25
|
|
|
@@ -3132,7 +3133,14 @@ const NO_CANVAS = {};
|
|
|
3132
3133
|
// line, so turning the stars off through it RESTYLED THE WHOLE BOARD -- which is not what a switch
|
|
3133
3134
|
// labelled "Star field" should do. `stars` gates the sky alone; where it is absent the old
|
|
3134
3135
|
// meaning stands, so every caller that never heard of it behaves exactly as before.
|
|
3135
|
-
|
|
3136
|
+
// THE LIVING SKY IS NOT THE STAR FIELD (operator, 2026-09-16: "The sky is not drawing for me ...
|
|
3137
|
+
// The background is all black for me"). With the living sky chosen and the Star field switch off,
|
|
3138
|
+
// this gate turned off the day as well: a blue afternoon and a moonrise were being hidden by a
|
|
3139
|
+
// switch about stars. A board that asks for the living sky draws it whatever that switch says;
|
|
3140
|
+
// `stars` still decides the star field, which the living sky brings out at night by itself.
|
|
3141
|
+
// the Earth sky (settings.js calls it 'earth'; 'living' was its name for a day and still answers)
|
|
3142
|
+
const earthSky = (o) => o.skyType === 'earth' || o.skyType === 'living';
|
|
3143
|
+
const starsOn = (o) => earthSky(o) || !!(o.stars ?? o.space);
|
|
3136
3144
|
// THE GALAXY (operator, 2026-09-12: "I want all the starts slowly rotating to form a spiral
|
|
3137
3145
|
// galaxy in the background ... Make it a toggle").
|
|
3138
3146
|
//
|
|
@@ -3688,7 +3696,12 @@ function paintFrame(ctx, geom, frame, opts, view, gridN, blockRows, gridH = grid
|
|
|
3688
3696
|
ctx.clearRect(0, 0, pw, ph);
|
|
3689
3697
|
ctx.fillStyle = opts.background;
|
|
3690
3698
|
ctx.fillRect(0, 0, pw, ph);
|
|
3691
|
-
|
|
3699
|
+
// THE SKY: the star field, or the living sky (livingsky.js: a real day from the clock, with the
|
|
3700
|
+
// star field coming out at night through the same drawStars)
|
|
3701
|
+
if (starsOn(opts)) {
|
|
3702
|
+
if (earthSky(opts)) drawLivingSky(ctx, pw, ph, dpr || 1, view.now ?? 0, opts, { softStops, drawStars: (o) => drawStars(ctx, pw, ph, dpr || 1, view.now ?? 0, o) });
|
|
3703
|
+
else drawStars(ctx, pw, ph, dpr || 1, view.now ?? 0, opts);
|
|
3704
|
+
}
|
|
3692
3705
|
// with nothing on the board -- every block in the air between two layouts (a viewer-mode switch)
|
|
3693
3706
|
// -- the oblique board still draws itself: its transform is a constant, not fitted to the blocks
|
|
3694
3707
|
if (!frame.bounds && !opts.oblique) return;
|
|
@@ -3757,6 +3770,13 @@ function paintFrame(ctx, geom, frame, opts, view, gridN, blockRows, gridH = grid
|
|
|
3757
3770
|
const agentDraw = view.fx?.kind ? AGENTS[view.fx.kind]?.draw : null;
|
|
3758
3771
|
if (agentDraw) agentDraw(ctx, view, ctx.lineWidth, { drawCycles, drawBall, project });
|
|
3759
3772
|
else { drawCycles(ctx, view, ctx.lineWidth); drawBall(ctx, view, ctx.lineWidth); drawFireworks(ctx, view, ctx.lineWidth); drawSupernova(ctx, view, ctx.lineWidth); drawBlackHole(ctx, view, ctx.lineWidth); drawScanCurtain(ctx, view, ctx.lineWidth); }
|
|
3773
|
+
// A CALLER'S OWN LAYER, over the board and in the board's own projection (operator, 2026-09-16:
|
|
3774
|
+
// "We have fucking firework and nebula effects and you're doing shitty block and sprite
|
|
3775
|
+
// explosions?"). The effects above are the renderer's, chosen by cadence and seed; a game needs
|
|
3776
|
+
// to draw ITS events -- a shell landing here, at this instant -- with the same primitives. The
|
|
3777
|
+
// hook hands over the projector and softStops, so an explosion is built the way the fireworks
|
|
3778
|
+
// are rather than out of tiles, and lands exactly where the cell it destroyed was.
|
|
3779
|
+
if (typeof opts.overlay === 'function') opts.overlay(ctx, view, { project, softStops, lw: ctx.lineWidth });
|
|
3760
3780
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3761
3781
|
}
|
|
3762
3782
|
|
|
@@ -4104,6 +4124,7 @@ export function render3d(canvas, cells, options = {}) {
|
|
|
4104
4124
|
opts.starDensity, opts.starBrightness, opts.galaxy === true, opts.galaxyAt,
|
|
4105
4125
|
opts.nebulae !== false, opts.galaxies !== false, opts.dust !== false, opts.clusters !== false,
|
|
4106
4126
|
opts.starColours !== false, opts.starGlints !== false,
|
|
4127
|
+
opts.skyType, opts.skyClock, opts.skyHour, opts.skyWeather, opts.skyCover, opts.skyLat, opts.skyRays !== false, opts.skyRainbow === true, opts.skyShooting !== false, opts.skyHorizon, opts.skyMoon,
|
|
4107
4128
|
opts.neon === true, opts.sheen === true, opts.sheenStyle, opts.overheadLight === true, opts.light,
|
|
4108
4129
|
opts.neonSource, opts.neonColour, opts.neonBrightness, opts.wireWidth,
|
|
4109
4130
|
opts.transition ? `${opts.transition.rise}/${opts.transition.travel}/${opts.transition.drop}` : 'default'].join('|');
|
|
@@ -4212,6 +4233,8 @@ export function render3d(canvas, cells, options = {}) {
|
|
|
4212
4233
|
neon: opts.neon === true, sheen: opts.sheen === true, sheenStyle: opts.sheenStyle,
|
|
4213
4234
|
overheadLight: opts.overheadLight === true, // the lamp straight above (Tetrust)
|
|
4214
4235
|
light: opts.light, // or wherever settings.js space.light puts it
|
|
4236
|
+
lightHeight: opts.lightHeight,
|
|
4237
|
+
lightGain: opts.lightGain, topLight: opts.topLight, // how hard the lamp is, and how bright the tops sit under it
|
|
4215
4238
|
neonSource: opts.neonSource, neonColour: opts.neonColour, neonBrightness: opts.neonBrightness,
|
|
4216
4239
|
wireWidth: opts.wireWidth,
|
|
4217
4240
|
// the paint order's memory across frames (blockscene3d obliqueOrder): a tangle keeps the
|
package/public/js/explorer.js
CHANGED
|
@@ -19,8 +19,14 @@
|
|
|
19
19
|
|
|
20
20
|
const X = { key: null, data: null, error: null, loading: false, shown: null, bound: false };
|
|
21
21
|
|
|
22
|
+
// A malformed escape in a pasted link (`#explorer/tx/%E0`) made decodeURIComponent throw inside the
|
|
23
|
+
// render, and the explorer stopped drawing for that tab (audit 2026-09-16, I1). Such a link is a
|
|
24
|
+
// route to the explorer's home, not an exception.
|
|
25
|
+
const decodePart = (x) => { try { return decodeURIComponent(x); } catch { return null; } };
|
|
22
26
|
export function parseRoute(r) {
|
|
23
|
-
const
|
|
27
|
+
const parts = String(r ?? '').split('/').filter((x) => x !== '').map(decodePart);
|
|
28
|
+
if (parts.includes(null)) return { kind: 'home' };
|
|
29
|
+
const [kind, id, page] = parts;
|
|
24
30
|
if ((kind === 'block' || kind === 'address') && id) return { kind, id, page: Math.max(0, Math.floor(Number(page) || 0)) };
|
|
25
31
|
if (kind === 'tx' && id) return { kind, id };
|
|
26
32
|
return { kind: 'home' };
|