// 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);