Minimap with zoom in the bottom right, full map on M; cards move to the bottom centre (t32)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 19:23:43 -04:00
co-authored by Claude Opus 5.5
parent c069abe2d2
commit 8d12b67c08
16 changed files with 244 additions and 18 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// Discovery cards, hover info (I) and the cursor label.
// ---------- Discovery cards ----------
// Short cards in the lower right when you find or make something new. They fade
// Short cards at the bottom centre when you find or make something new. They fade
// on their own (hover holds them); everything they say is also in the notebook.
function showCard(c) {
if (!SET.cardSeconds) return;
+3 -1
View File
@@ -6,8 +6,10 @@ window.addEventListener('keydown', e => {
if ((e.key === 'Control' || e.key === 'Meta') && !GAME.ctrl) { GAME.ctrl = true; lastBagKey = ''; hudDirty = true; }
if (isTyping(e)) { if (e.key === 'Escape') e.target.blur(); return; }
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
if (GAME.running && k === 'm' && e.shiftKey && !e.repeat) { SET.muted = !SET.muted; saveSettings(); auVolumes(); toast(SET.muted ? 'Sound muted.' : 'Sound on.'); return; }
if (anyModal()) {
if (k === 'Escape') { e.preventDefault(); const t = topModal(); closeModal(t); }
else if (k === 'm' && !e.repeat && topModal() === 'm-map') closeModal('m-map');
else if (k === 'n' && !$('m-notebook').hidden) closeModal('m-notebook');
return;
}
@@ -36,7 +38,7 @@ window.addEventListener('keydown', e => {
case 'l': bot.lamp = !bot.lamp; playSfx('ui'); break;
case 'n': openNotebook(); break;
case 'b': openNotebook('Blueprints'); break;
case 'm': SET.muted = !SET.muted; saveSettings(); auVolumes(); toast(SET.muted ? 'Sound muted.' : 'Sound on.'); break;
case 'm': openMap(); break;
case '[': setBrush(-1); break;
case ']': setBrush(1); break;
case 'Delete': if (ghostBlocks.size || ghostMachines.length || ghostMolds.length) { clearAllGhosts(); toast('Blueprint ghosts cleared.'); } break;
+1
View File
@@ -82,6 +82,7 @@ function frame(now) {
if (document.body.classList.contains('paused') !== GAME.paused) document.body.classList.toggle('paused', GAME.paused);
updateOverlayUI();
if (now > ambientT) { ambientT = now + 200; auAmbient(scanVisible()); }
updateMap(now);
frameMs = frameMs * 0.9 + (performance.now() - t0) * 0.1;
fpsSm = fpsSm * 0.95 + (1 / Math.max(0.001, dt)) * 0.05;
if (SET.showPerf && R.frame % 15 === 0) {
+93
View File
@@ -0,0 +1,93 @@
// Shared world colours, minimap and full map.
let MAP_S = 2;
const MAP = { canvas: document.createElement('canvas'), image: null, w: 0, h: 0, init: 0, rr: 0, near: 0, miniT: 0, fullT: 0, x: 0, y: 0, zoom: 1, drag: null };
function resetMap() {
MAP_S = W > 4096 ? 4 : 2; MAP.w = W; MAP.h = H; MAP.fog = SET.fog;
MAP.canvas.width = Math.ceil(W / MAP_S); MAP.canvas.height = Math.ceil(H / MAP_S);
MAP.ctx = MAP.canvas.getContext('2d'); MAP.image = MAP.ctx.createImageData(MAP.canvas.width, MAP.canvas.height);
MAP.init = MAP.rr = MAP.near = MAP.miniT = MAP.fullT = 0;
if (![1, 2, 4, 8, 16].includes(SET.minimapZoom)) SET.minimapZoom = 4;
}
function mapArea(cx, cy) {
const s = MAP_S, x0 = cx * 32, y0 = cy * 32, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32), data = MAP.image.data, mw = MAP.canvas.width;
for (let y = y0; y < y1; y += s) for (let x = x0; x < x1; x += s) {
const xx = Math.min(W - 1, x + s / 2), yy = Math.min(H - 1, y + s / 2), seen = SET.fog ? fogSeen(xx, yy) : 255;
let m = mat[yy * W + xx];
if (!m || PH[m] === PH_GAS) for (let c = 0; c < 4; c++) {
const k = mat[Math.min(H - 1, y + (c >> 1) * (s - 1)) * W + Math.min(W - 1, x + (c & 1) * (s - 1))];
if (k && PH[k] !== PH_GAS) { m = k; break; }
}
const sky = yy < surfY[xx], f = seen < 64 ? 0 : seen / 255, i = (y / s * mw + x / s) * 4;
data[i] = (m ? COLR[m] : sky ? 78 : 20) * f;
data[i + 1] = (m ? COLG[m] : sky ? 114 : 18) * f;
data[i + 2] = (m ? COLB[m] : sky ? 142 : 17) * f; data[i + 3] = 255;
}
MAP.ctx.putImageData(MAP.image, 0, 0, x0 / s, y0 / s, (x1 - x0) / s, (y1 - y0) / s);
}
function updateMap(now) {
if (!GAME.running || GAME.loading) return;
$('minimap').hidden = !SET.minimap;
if (!SET.minimap && $('m-map').hidden) return;
if (MAP.w !== W || MAP.h !== H || MAP.fog !== SET.fog) resetMap();
const nx = Math.ceil(W / 32), ny = Math.ceil(H / 32), total = nx * ny;
if (MAP.init < total) {
const end = performance.now() + 8;
do { const n = MAP.init++; mapArea(n % nx, Math.floor(n / nx)); } while (MAP.init < total && performance.now() < end);
if (MAP.init === total) MAP.fullT = MAP.miniT = 0;
} else {
mapArea(Math.max(0, Math.min(nx - 1, Math.floor(bot.x / 32))), Math.max(0, Math.min(ny - 1, Math.floor(bot.y / 32))));
const rx = Math.ceil(R.bw / 32), ry = Math.ceil(R.bh / 32), span = rx * 2 + 1;
for (let n = 0; n < 15; n++) {
const k = MAP.near++ % (span * (ry * 2 + 1)), x = Math.floor(bot.x / 32) - rx + k % span, y = Math.floor(bot.y / 32) - ry + Math.floor(k / span);
if (x >= 0 && y >= 0 && x < nx && y < ny) mapArea(x, y);
}
for (let n = 0; n < 8; n++) { const k = MAP.rr++ % total; mapArea(k % nx, Math.floor(k / nx)); }
}
if (SET.minimap && now >= MAP.miniT) { MAP.miniT = now + 100; drawMap($('mini-canvas'), bot.x, bot.y, 1 / SET.minimapZoom, false); }
if (!$('m-map').hidden && now >= MAP.fullT) { MAP.fullT = now + 1000; drawFullMap(); }
}
function drawMap(c, x, y, z, full) {
const ctx = c.getContext('2d'), w = c.width, h = c.height;
ctx.imageSmoothingEnabled = false; ctx.fillStyle = '#080706'; ctx.fillRect(0, 0, w, h);
ctx.drawImage(MAP.canvas, w / 2 - x * z, h / 2 - y * z, W * z, H * z);
const marker = (mx, my, colour, label) => {
const px = w / 2 + (mx - x) * z, py = h / 2 + (my - y) * z;
if (px < 0 || py < 0 || px > w || py > h) return;
ctx.fillStyle = '#080706'; ctx.fillRect(px - 4, py - 4, 8, 8); ctx.fillStyle = colour; ctx.fillRect(px - 2, py - 2, 4, 4);
if (full) { ctx.font = '13px sans-serif'; ctx.lineWidth = 3; ctx.strokeStyle = '#080706'; const ly = label === 'Trade dock' ? py + 17 : py - 7; ctx.strokeText(label, px + 7, ly); ctx.fillText(label, px + 7, ly); }
};
for (const m of machines) if (m && m.type === 'dock' && (!SET.fog || fogSeen(m.x, m.y) >= 64)) marker(m.x + m.w / 2, m.y + m.h / 2, '#e59a52', 'Trade dock');
marker(bot.x, bot.y, '#c9fff2', 'You are here');
}
function mapFit() { const c = $('map-canvas'); return Math.min(c.clientWidth / W, c.clientHeight / H); }
function drawFullMap() {
if (GAME.loading || !GAME.running || $('m-map').hidden || !MAP.image) return;
const c = $('map-canvas'); c.width = Math.max(1, c.clientWidth); c.height = Math.max(1, c.clientHeight);
MAP.zoom = Math.max(mapFit(), Math.min(4, MAP.zoom));
const hx = c.width / (2 * MAP.zoom), hy = c.height / (2 * MAP.zoom);
MAP.x = hx >= W / 2 ? W / 2 : Math.max(hx, Math.min(W - hx, MAP.x));
MAP.y = hy >= H / 2 ? H / 2 : Math.max(hy, Math.min(H - hy, MAP.y));
drawMap(c, MAP.x, MAP.y, MAP.zoom, true);
}
function openMap() {
if (!GAME.running || GAME.loading) return;
openModal('m-map'); MAP.x = bot.x; MAP.y = bot.y; MAP.zoom = mapFit(); drawFullMap();
}
function zoomMap(factor, px, py) {
const c = $('map-canvas'), old = MAP.zoom, z = Math.max(mapFit(), Math.min(4, old * factor));
MAP.x += ((px === undefined ? c.width / 2 : px) - c.width / 2) * (1 / old - 1 / z);
MAP.y += ((py === undefined ? c.height / 2 : py) - c.height / 2) * (1 / old - 1 / z);
MAP.zoom = z; drawFullMap();
}
function zoomMinimap(d) { SET.minimapZoom = Math.max(1, Math.min(16, SET.minimapZoom * (d > 0 ? 0.5 : 2))); saveSettings(); MAP.miniT = 0; }
$('mini-plus').onclick = () => zoomMinimap(1);
$('mini-minus').onclick = () => zoomMinimap(-1);
$('mini-map').onclick = openMap;
$('map-plus').onclick = () => zoomMap(2);
$('map-minus').onclick = () => zoomMap(0.5);
$('map-centre').onclick = () => { MAP.x = bot.x; MAP.y = bot.y; drawFullMap(); };
$('map-canvas').addEventListener('wheel', e => { e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); zoomMap(e.deltaY < 0 ? 1.25 : 0.8, e.clientX - r.left, e.clientY - r.top); }, { passive: false });
$('map-canvas').onpointerdown = e => { if (e.button !== 0) return; MAP.drag = [e.clientX, e.clientY]; e.currentTarget.setPointerCapture(e.pointerId); };
$('map-canvas').onpointermove = e => { if (!MAP.drag) return; MAP.x -= (e.clientX - MAP.drag[0]) / MAP.zoom; MAP.y -= (e.clientY - MAP.drag[1]) / MAP.zoom; MAP.drag = [e.clientX, e.clientY]; drawFullMap(); };
$('map-canvas').onpointerup = $('map-canvas').onpointercancel = () => { MAP.drag = null; };
window.addEventListener('resize', drawFullMap);
+1 -1
View File
@@ -174,7 +174,7 @@ function controlsHtml() {
['Tools', '1 – 4'], ['Select one to blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall or mold material menu (Build)', 'F'],
['Pick up selection or what you click (Pneumatic nozzle)', 'Left drag · Shift: everything'], ['Select (Pneumatic nozzle)', 'Click a bag name · Q/E'], ['Add to selection (Pneumatic nozzle)', 'Ctrl+click a bag name'], ['Show everything found (Pneumatic nozzle)', 'Hold Ctrl in the bag'], ['Clear selection (Pneumatic nozzle)', 'Clear in the bag'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
['Replace what is built (Build)', 'Hold G'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and tube directions', 'I'], ['Bot lamp', 'L'],
['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'M'],
['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'Shift + M'], ['Full map', 'M'], ['Minimap zoom', '+ / − on minimap'],
];
const pad = [
['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
+4 -4
View File
@@ -16,7 +16,7 @@ function enterGame() {
GAME.status = ''; GAME.statusT = 0;
$('title').hidden = true; $('hud').hidden = false;
GAME.running = true; GAME.paused = false;
buildSky(); resizeRender(); snapCamera();
buildSky(); resizeRender(); snapCamera(); resetMap();
GAME.lastAutosave = performance.now();
GAME.drag = null; GAME.lmb = GAME.rmb = false; lastBagKey = '';
hudDirty = true; renderHotbar();
@@ -97,11 +97,11 @@ async function refreshContinue() {
}
// ---------- Modals ----------
function anyModal() { return ['m-pause', 'm-settings', 'm-notebook', 'm-worlds', 'm-dialog'].some(id => !$(id).hidden); }
function anyModal() { return ['m-pause', 'm-settings', 'm-notebook', 'm-worlds', 'm-map', 'm-dialog'].some(id => !$(id).hidden); }
function openModal(id) { $(id).hidden = false; GAME.paused = true; setTimeout(() => { const f = $(id).querySelector('button, select, input'); if (f && !GAME.usingPadMenu) { /* keep focus neutral for mouse users */ } }, 0); }
function closeModal(id) { $(id).hidden = true; if (id === 'm-dialog') GAME.editRun = null; if (!anyModal()) GAME.paused = false; }
function closeAllModals() { ['m-pause', 'm-settings', 'm-notebook', 'm-worlds', 'm-dialog'].forEach(id => $(id).hidden = true); GAME.paused = false; }
function topModal() { for (const id of ['m-dialog', 'm-worlds', 'm-settings', 'm-notebook', 'm-pause']) if (!$(id).hidden) return id; return null; }
function closeAllModals() { ['m-pause', 'm-settings', 'm-notebook', 'm-worlds', 'm-map', 'm-dialog'].forEach(id => $(id).hidden = true); GAME.paused = false; }
function topModal() { for (const id of ['m-dialog', 'm-map', 'm-worlds', 'm-settings', 'm-notebook', 'm-pause']) if (!$(id).hidden) return id; return null; }
function dialog(title, bodyHtml, buttons) {
$('dlg-title').textContent = title; $('dlg-body').innerHTML = bodyHtml;
const foot = $('dlg-foot'); foot.innerHTML = '';
+1
View File
@@ -9,6 +9,7 @@ const SET_TABS = {
{ k: 'fpsCap', label: 'Frame cap', d: 'Uncapped follows your display refresh rate.', type: 'select', opts: [[30, '30 fps'], [60, '60 fps'], [120, '120 fps'], [0, 'Uncapped']] },
{ k: 'fullscreen', label: 'Fullscreen', type: 'button', text: 'Toggle fullscreen', fn: toggleFullscreen },
{ k: 'cursorLabel', label: 'Name next to the cursor', d: 'Shows what you are pointing at right beside the cursor.', type: 'toggle' },
{ k: 'minimap', label: 'Show minimap', type: 'toggle' },
{ k: 'showPerf', label: 'Performance stats', d: 'Frame time, simulation time and active areas.', type: 'toggle' },
{ k: 'cameraSmooth', label: 'Camera smoothing', type: 'range', min: 0, max: 0.95, step: 0.05, fmt: v => Math.round(v * 100) + '%' },
{ k: 'cameraLead', label: 'Camera looks toward the pointer', d: 'The view drifts toward the mouse or right stick. Turn off to keep the bot centred.', type: 'toggle' },
+1 -1
View File
@@ -5,7 +5,7 @@
const SET_DEFAULT = {
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true,
look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
minimap: true, minimapZoom: 4, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: true, sv: 2,
};