// PROCITY Lane B — minimap.js // The `map` mode: a 2D top-down town directory drawn from the CityPlan (streets + lots coloured by // shop type + a live player dot/heading). This is the shell's own lightweight map; Lane A's // map.html debug view grows into the richer directory later (Lane F reconciles). const TYPE_COLOR = { record: '#8a5aa8', opshop: '#8a7a5a', toy: '#c86aa0', book: '#7a6a3a', video: '#4a6ab0', pawn: '#c4a028', milkbar: '#c4863a', dept: '#5a6a7a', stall: '#5a8a6a', house: '#6b5a48', anchor: '#5a6a7a', }; export function createMinimap(plan) { const wrap = document.createElement('div'); wrap.id = 'pc-map'; wrap.style.cssText = 'position:fixed;inset:0;z-index:20;display:none;background:#141712;' + 'align-items:center;justify-content:center;flex-direction:column;font:13px -apple-system,sans-serif;color:#e8e0d0'; const canvas = document.createElement('canvas'); canvas.width = 720; canvas.height = 720; canvas.style.cssText = 'background:#1b1e17;border-radius:10px;max-width:92vw;max-height:80vh;box-shadow:0 8px 40px #000'; const cap = document.createElement('div'); cap.style.cssText = 'margin-top:12px;opacity:.8'; cap.textContent = `${plan.name} — seed ${plan.citySeed} · press M to close`; wrap.append(canvas, cap); document.body.appendChild(wrap); const ctx = canvas.getContext('2d'); // world→canvas transform (fit plan extent with margin) let minX = -60, maxX = 60, minZ = -60, maxZ = 60; for (const n of plan.streets.nodes) { minX = Math.min(minX, n.x); maxX = Math.max(maxX, n.x); minZ = Math.min(minZ, n.z); maxZ = Math.max(maxZ, n.z); } for (const l of plan.lots) { minX = Math.min(minX, l.x); maxX = Math.max(maxX, l.x); minZ = Math.min(minZ, l.z); maxZ = Math.max(maxZ, l.z); } const pad = 24, W = canvas.width, H = canvas.height; const span = Math.max(maxX - minX, maxZ - minZ) + 40; const cxw = (minX + maxX) / 2, czw = (minZ + maxZ) / 2; const sc = (W - pad * 2) / span; const tx = (x) => W / 2 + (x - cxw) * sc; const tz = (z) => H / 2 + (z - czw) * sc; const nodes = new Map(plan.streets.nodes.map((n) => [n.id, n])); const shopByLot = new Map(plan.shops.map((s) => [s.lot, s])); function drawStatic() { ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#1b1e17'; ctx.fillRect(0, 0, W, H); // streets for (const e of plan.streets.edges) { const a = nodes.get(e.a), b = nodes.get(e.b); ctx.strokeStyle = '#3a3f34'; ctx.lineWidth = Math.max(3, e.width * sc); ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(tx(a.x), tz(a.z)); ctx.lineTo(tx(b.x), tz(b.z)); ctx.stroke(); } // lots for (const l of plan.lots) { const s = shopByLot.get(l.id); const col = TYPE_COLOR[s ? s.type : l.use] || '#666'; ctx.save(); ctx.translate(tx(l.x), tz(l.z)); ctx.rotate(-(l.ry || 0)); ctx.fillStyle = col; ctx.fillRect(-l.w * sc / 2, -l.d * sc / 2, l.w * sc, l.d * sc); // front-edge tick (toward the street) ctx.fillStyle = 'rgba(255,255,255,.5)'; ctx.fillRect(-l.w * sc / 2, l.d * sc / 2 - 2, l.w * sc, 2); ctx.restore(); } } function draw(playerPos, fwd) { drawStatic(); const x = tx(playerPos.x), z = tz(playerPos.z); // heading (world +X→right, +Z→down on the map) ctx.strokeStyle = '#ffd75e'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x, z); ctx.lineTo(x + fwd.x * 16, z + fwd.z * 16); ctx.stroke(); ctx.fillStyle = '#ffd75e'; ctx.beginPath(); ctx.arc(x, z, 6, 0, Math.PI * 2); ctx.fill(); } return { draw, setVisible: (v) => { wrap.style.display = v ? 'flex' : 'none'; }, dispose: () => wrap.remove(), }; }