dungeon map maker

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
    <title>Dungeon Sketcher Pro</title>
    <script src=”https://cdn.tailwindcss.com”></script>
    <style>
        body {
            background-color: #121212;
            user-select: none;
            touch-action: none;
            color: #e5e7eb;
            font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            overflow: hidden;
        }
        #viewport {
            width: 100vw;
            height: 100vh;
            overflow: hidden;
            position: relative;
            background-color: #1a1a1a;
        }
        #canvas-wrapper {
            position: absolute;
            transform-origin: 0 0;
        }
        canvas {
            display: block;
            cursor: crosshair;
            background-color: #f4e4bc;
        }
        .tool-btn {
            transition: all 0.1s ease-out;
            border: 1px solid transparent;
        }
        .tool-btn.active {
            background-color: #d97706 !important;
            color: white !important;
            border-color: #f59e0b;
            transform: scale(1.02);
            box-shadow: 0 4px 12px rgba(217, 119, 6, 0.4);
        }
        .sidebar {
            transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            position: fixed;
            top: 0;
            left: 0;
            height: 100vh;
            z-index: 50;
            box-shadow: 10px 0 30px rgba(0,0,0,0.5);
            backdrop-filter: blur(12px);
        }
        .sidebar::-webkit-scrollbar { width: 4px; }
        .sidebar::-webkit-scrollbar-thumb { background: #444; border-radius: 10px; }
        .sidebar.hidden-menu { transform: translateX(-100%); box-shadow: none; }
       
        .floating-toggle { position: fixed; bottom: 1.5rem; left: 1.5rem; z-index: 100; }
       
        /* Export Modal */
        #snapshot-overlay {
            display: none;
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.98);
            z-index: 200;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 10px;
        }
        .modal-content {
            background: #1c1917;
            border: 1px solid #44403c;
            border-radius: 12px;
            padding: 16px;
            max-width: 95%;
            max-height: 95vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 16px;
            box-shadow: 0 20px 50px rgba(0,0,0,0.5);
        }
        #snapshot-img {
            max-width: 100%;
            max-height: 50vh;
            border: 4px solid #fff;
            background-color: #f4e4bc;
            object-fit: contain;
            pointer-events: auto;
        }

        .status-badge {
            position: fixed;
            top: 1rem;
            right: 1rem;
            background: rgba(0,0,0,0.8);
            padding: 0.5rem 1rem;
            border-radius: 8px;
            font-size: 0.7rem;
            pointer-events: none;
            z-index: 40;
            border: 1px solid rgba(255,255,255,0.1);
        }
        label { color: #9ca3af; font-size: 0.6rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.1em; display: block; margin-bottom: 0.3rem; }
    </style>
</head>
<body class=”flex h-screen”>

    <div class=”status-badge” id=”status”>Inking…</div>

    <aside id=”menu” class=”sidebar w-64 bg-stone-900/90 border-r border-stone-800 p-5 flex flex-col gap-4 overflow-y-auto”>
        <div class=”mb-2″>
            <h1 class=”text-xl font-serif font-bold text-amber-500″>Dungeon Sketcher</h1>
            <p class=”text-[9px] text-stone-500 uppercase tracking-[0.2em]”>Ink & Paper v6.5</p>
        </div>

        <div>
            <label>Architecture</label>
            <div class=”grid grid-cols-2 gap-2″>
                <button onclick=”setMode(‘room’)” id=”btn-room” class=”tool-btn active bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>🎨</span>Room</button>
                <button onclick=”setMode(‘door’)” id=”btn-door” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>🚪</span>Door</button>
                <button onclick=”setMode(‘thinwall’)” id=”btn-thinwall” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>📏</span>Divider</button>
                <button onclick=”setMode(‘stairs’)” id=”btn-stairs” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>🪜</span>Stairs</button>
            </div>
        </div>

        <div>
            <label>Terrain</label>
            <div class=”grid grid-cols-3 gap-2″>
                <button onclick=”setMode(‘cavern’)” id=”btn-cavern” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>⛰️</span></button>
                <button onclick=”setMode(‘water’)” id=”btn-water” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>🌊</span></button>
                <button onclick=”setMode(‘lava’)” id=”btn-lava” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>🔥</span></button>
            </div>
        </div>

        <div>
            <label>Monsters</label>
            <div class=”grid grid-cols-3 gap-2″>
                <button onclick=”setMode(‘mon_skull’)” id=”btn-mon_skull” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>💀</span></button>
                <button onclick=”setMode(‘mon_slime’)” id=”btn-mon_slime” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>💧</span></button>
                <button onclick=”setMode(‘mon_eye’)” id=”btn-mon_eye” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>👁️</span></button>
                <button onclick=”setMode(‘mon_ghost’)” id=”btn-mon_ghost” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>👻</span></button>
                <button onclick=”setMode(‘mon_boss’)” id=”btn-mon_boss” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>👹</span></button>
            </div>
        </div>

        <div>
            <label>Items</label>
            <div class=”grid grid-cols-3 gap-2″>
                <button onclick=”setMode(‘chest’)” id=”btn-chest” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>🧳</span></button>
                <button onclick=”setMode(‘treasure’)” id=”btn-treasure” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>💎</span></button>
                <button onclick=”setMode(‘trap’)” id=”btn-trap” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>⚠️</span></button>
                <button onclick=”setMode(‘pillar’)” id=”btn-pillar” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>⚪</span></button>
                <button onclick=”setMode(‘fountain’)” id=”btn-fountain” class=”tool-btn bg-stone-800 p-2 rounded-lg flex flex-col items-center”><span>⛲</span></button>
            </div>
        </div>

        <div class=”mt-auto pt-4 space-y-2 border-t border-stone-800 pb-20 md:pb-4″>
            <button onclick=”clearMap()” class=”w-full bg-red-900/20 hover:bg-red-900/40 text-red-400 py-2 rounded text-[10px] font-bold uppercase”>Reset</button>
            <button onclick=”showSnapshot()” class=”w-full bg-amber-600 hover:bg-amber-500 text-white py-3 rounded text-sm font-bold shadow-lg uppercase tracking-wider”>Export Map</button>
        </div>
    </aside>

    <!– Export Modal –>
    <div id=”snapshot-overlay”>
        <div class=”modal-content”>
            <div class=”text-center”>
                <h2 class=”text-amber-500 font-bold text-lg font-serif”>Export Map</h2>
                <p class=”text-stone-400 text-xs mt-1″>Option 1: Long-press image to Save</p>
            </div>
           
            <img id=”snapshot-img” src=”” alt=”Dungeon Map”>
           
            <div class=”w-full grid grid-cols-1 gap-2″>
                <p class=”text-stone-500 text-[10px] text-center uppercase tracking-widest mt-2″>Export Actions</p>
                <button onclick=”downloadImage()” class=”w-full bg-stone-700 hover:bg-stone-600 text-white py-3 rounded font-bold text-sm”>Download File</button>
                <button onclick=”openInNewTab()” class=”w-full bg-stone-700 hover:bg-stone-600 text-white py-3 rounded font-bold text-sm”>Open in New Tab</button>
                <button onclick=”hideSnapshot()” class=”w-full bg-red-900/50 hover:bg-red-900/80 text-red-200 py-2 rounded font-bold text-xs mt-2″>Close</button>
            </div>
        </div>
    </div>

    <main id=”viewport” class=”flex-1 overflow-hidden”>
        <div id=”canvas-wrapper”>
            <canvas id=”dungeonCanvas”></canvas>
        </div>
    </main>

    <button onclick=”toggleMenu()” class=”floating-toggle bg-stone-800 hover:bg-stone-700 text-amber-500 p-4 rounded-full shadow-2xl border border-stone-600 transition-all active:scale-95″>
        <svg class=”w-6 h-6″ fill=”none” stroke=”currentColor” viewBox=”0 0 24 24″><path stroke-linecap=”round” stroke-linejoin=”round” stroke-width=”2″ d=”M4 6h16M4 12h16M4 18h16″></path></svg>
    </button>

<script>
    const canvas = document.getElementById(‘dungeonCanvas’);
    const ctx = canvas.getContext(‘2d’);
    const wrapper = document.getElementById(‘canvas-wrapper’);
    const viewport = document.getElementById(‘viewport’);
   
    let gridSize = 40, rows = 30, cols = 40;
    let currentMode = ‘room’, isDrawing = false, lastCell = { r: -1, c: -1 };
    let zoom = 1.0, panX = 40, panY = 40;
    let initialPinchDist = null, initialZoom = 1.0;

    let rooms = Array(rows).fill().map(() => Array(cols).fill(false));
    let flavor = Array(rows).fill().map(() => Array(cols).fill(null));
    let furniture = Array(rows).fill().map(() => Array(cols).fill(null));
    let hDoors = Array(rows + 1).fill().map(() => Array(cols).fill(false));
    let vDoors = Array(rows).fill().map(() => Array(cols + 1).fill(false));
    let hThinWalls = Array(rows + 1).fill().map(() => Array(cols).fill(false));
    let vThinWalls = Array(rows).fill().map(() => Array(cols + 1).fill(false));

    function initCanvas() {
        canvas.width = cols * gridSize;
        canvas.height = rows * gridSize;
        updateTransform();
        render();
        updateStatus();
    }

    function updateTransform() { wrapper.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; }
    function updateZoom(val) { zoom = parseFloat(val); updateTransform(); updateStatus(); }
    function updateStatus() { document.getElementById(‘status’).innerText = `${cols}x${rows} | ${Math.round(zoom * 100)}%`; }
    function toggleMenu() { document.getElementById(‘menu’).classList.toggle(‘hidden-menu’); }

    function setMode(mode) {
        currentMode = mode;
        document.querySelectorAll(‘.tool-btn’).forEach(btn => btn.classList.remove(‘active’));
        document.getElementById(`btn-${mode}`)?.classList.add(‘active’);
    }

    // Pseudo-random generator for consistent sketchiness per cell
    function pseudoRandom(x, y) {
        return Math.abs(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453) % 1;
    }

    function drawSketchLine(x1, y1, x2, y2, color = ‘#1a1a1a’, width = 2.8, jitter = 1.2) {
        ctx.beginPath(); ctx.lineWidth = width; ctx.lineCap = ’round’; ctx.strokeStyle = color;
        const dx = x2 – x1, dy = y2 – y1, dist = Math.sqrt(dx*dx + dy*dy), steps = Math.max(2, Math.floor(dist / 6));
        ctx.moveTo(x1, y1);
        for(let i = 1; i <= steps; i++) {
            const t = i / steps;
            ctx.lineTo(x1 + dx * t + (Math.random()-0.5)*jitter, y1 + dy * t + (Math.random()-0.5)*jitter);
        }
        ctx.lineTo(x2, y2); ctx.stroke();
    }

    function drawSketchCircle(cx, cy, r, color = ‘#1a1a1a’, width = 2) {
        ctx.beginPath();
        ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineCap = ’round’;
        const steps = 12;
        const rVar = r * 0.1;
        for (let i = 0; i <= steps; i++) {
            const a = (i / steps) * Math.PI * 2;
            const ra = r + (Math.random() – 0.5) * rVar;
            const x = cx + Math.cos(a) * ra;
            const y = cy + Math.sin(a) * ra;
            if (i === 0) ctx.moveTo(x, y);
            else ctx.lineTo(x, y);
        }
        ctx.closePath();
        ctx.stroke();
    }

    function drawJaggedLine(x1, y1, x2, y2, color = ‘#1a1a1a’, width = 3) {
        ctx.beginPath(); ctx.lineWidth = width; ctx.strokeStyle = color;
        const dx = x2-x1, dy = y2-y1, steps = Math.max(3, Math.floor(Math.sqrt(dx*dx+dy*dy)/4));
        ctx.moveTo(x1, y1);
        for(let i=1; i<=steps; i++){
            const t = i/steps;
            ctx.lineTo(x1+dx*t+(Math.random()-0.5)*6, y1+dy*t+(Math.random()-0.5)*6);
        }
        ctx.lineTo(x2, y2); ctx.stroke();
    }

    function drawEdgeDoor(x, y, isVertical) {
        const aperture = gridSize * 0.45, half = aperture/2;
        ctx.fillStyle = ‘#f4e4bc’;
        if(isVertical){
            const ty = y + (gridSize-aperture)/2, by = y + (gridSize+aperture)/2;
            drawSketchLine(x, y, x, ty, ‘#1a1a1a’, 2.8, 0.5);
            drawSketchLine(x, y+gridSize, x, by, ‘#1a1a1a’, 2.8, 0.5);
            ctx.fillRect(x-half, ty, aperture, aperture);
            drawSketchLine(x, ty, x, by, ‘#1a1a1a’, 3, 0);
            drawSketchLine(x-half, ty, x+half, ty, ‘#1a1a1a’, 2, 0);
            drawSketchLine(x-half, by, x+half, by, ‘#1a1a1a’, 2, 0);
        } else {
            const lx = x + (gridSize-aperture)/2, rx = x + (gridSize+aperture)/2;
            drawSketchLine(x, y, lx, y, ‘#1a1a1a’, 2.8, 0.5);
            drawSketchLine(x+gridSize, y, rx, y, ‘#1a1a1a’, 2.8, 0.5);
            ctx.fillRect(lx, y-half, aperture, aperture);
            drawSketchLine(lx, y, rx, y, ‘#1a1a1a’, 3, 0);
            drawSketchLine(lx, y-half, lx, y+half, ‘#1a1a1a’, 2, 0);
            drawSketchLine(rx, y-half, rx, y+half, ‘#1a1a1a’, 2, 0);
        }
    }

    function render() {
        // 0. BAKE PARCHMENT TEXTURE
        ctx.fillStyle = ‘#f4e4bc’;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
       
        // Add Grain
        for(let i=0; i<2000; i++) {
            ctx.fillStyle = `rgba(0,0,0,${Math.random()*0.03})`;
            ctx.fillRect(Math.random()*canvas.width, Math.random()*canvas.height, 1.5, 1.5);
        }

        // Faint Hand-Drawn Grid
        ctx.strokeStyle = ‘rgba(0,0,0,0.06)’;
        ctx.lineWidth = 1;
        for(let i=0; i<=cols; i++){ ctx.beginPath(); ctx.moveTo(i*gridSize,0); ctx.lineTo(i*gridSize, canvas.height); ctx.stroke(); }
        for(let j=0; j<=rows; j++){ ctx.beginPath(); ctx.moveTo(0, j*gridSize); ctx.lineTo(canvas.width, j*gridSize); ctx.stroke(); }

        // 1. Terrain Layer
        for (let r=0; r<rows; r++) {
            for (let c=0; c<cols; c++) {
                const x = c*gridSize, y = r*gridSize;
                if(flavor[r][c] === ‘water’){ ctx.fillStyle = ‘#a8d1ff’; ctx.fillRect(x,y,gridSize,gridSize); }
                else if(flavor[r][c] === ‘lava’){ ctx.fillStyle = ‘#ff7e54’; ctx.fillRect(x,y,gridSize,gridSize); }
                else if(flavor[r][c] === ‘cavern’){ ctx.fillStyle = ‘#d4c4a1’; ctx.fillRect(x,y,gridSize,gridSize); }
                else if(rooms[r][c]){ ctx.fillStyle = ‘rgba(255, 255, 255, 0.45)’; ctx.fillRect(x,y,gridSize,gridSize); }
            }
        }

        // 2. Structural Walls
        for (let r=0; r<rows; r++) {
            for (let c=0; c<cols; c++) {
                if(!rooms[r][c]) continue;
                const x = c*gridSize, y = r*gridSize;
                const check = (nr, nc, x1, y1, x2, y2, isH) => {
                    const nR = (nr>=0 && nr<rows && nc>=0 && nc<cols) ? rooms[nr][nc] : false;
                    const dE = isH ? hDoors[isH===’top’?r:r+1][c] : vDoors[r][isH===’left’?c:c+1];
                    const nf = (nr>=0 && nr<rows && nc>=0 && nc<cols) ? flavor[nr][nc] : null;
                    if (!nR && !dE && ![‘cavern’,’lava’].includes(nf)) drawSketchLine(x1, y1, x2, y2);
                };
                check(r-1,c, x,y, x+gridSize,y, ‘top’); check(r+1,c, x,y+gridSize, x+gridSize,y+gridSize, ‘bottom’);
                check(r,c-1, x,y, x,y+gridSize, ‘left’); check(r,c+1, x+gridSize,y, x+gridSize,y+gridSize, ‘right’);
            }
        }

        // 3. Doors & Thin Walls
        for (let r=0; r<=rows; r++) {
            for (let c=0; c<cols; c++) {
                if(hDoors[r][c]) drawEdgeDoor(c*gridSize, r*gridSize, false);
                if(hThinWalls[r][c]) drawSketchLine(c*gridSize, r*gridSize, (c+1)*gridSize, r*gridSize, ‘#555’, 2.2, 0.2);
            }
        }
        for (let r=0; r<rows; r++) {
            for (let c=0; c<=cols; c++) {
                if(vDoors[r][c]) drawEdgeDoor(c*gridSize, r*gridSize, true);
                if(vThinWalls[r][c]) drawSketchLine(c*gridSize, r*gridSize, c*gridSize, (r+1)*gridSize, ‘#555’, 2.2, 0.2);
            }
        }

        // 4. Terrain Edges
        for (let r=0; r<rows; r++) {
            for (let c=0; c<cols; c++) {
                const x = c*gridSize, y = r*gridSize, f = flavor[r][c];
                if(![‘cavern’,’lava’].includes(f)) continue;
                const ds = (nr, nc, x1,y1,x2,y2) => {
                    const nf = (nr>=0 && nr<rows && nc>=0 && nc<cols) ? flavor[nr][nc] : null;
                    if(nf !== f) drawJaggedLine(x1,y1,x2,y2, f===’lava’?’#900′:’#1a1a1a’, f===’lava’?2:3.2);
                };
                ds(r-1,c, x,y, x+gridSize,y); ds(r+1,c, x,y+gridSize, x+gridSize,y+gridSize);
                ds(r,c-1, x,y, x,y+gridSize); ds(r,c+1, x+gridSize,y, x+gridSize,y+gridSize);
            }
        }

        // 5. Tokens
        for (let r=0; r<rows; r++) {
            for (let c=0; c<cols; c++) {
                const type = furniture[r][c]; if(!type) continue;
                const x = c*gridSize, y = r*gridSize, cx = x+gridSize/2, cy = y+gridSize/2;
                const rand = pseudoRandom(r, c);
               
                // Varied Rotation based on cell
                ctx.save();
                ctx.translate(cx, cy);
                ctx.rotate((rand – 0.5) * 0.4);
                ctx.translate(-cx, -cy);

                // Sketchy Base
                if(type.startsWith(‘mon_’) || [‘chest’,’treasure’,’trap’,’fountain’,’pillar’].includes(type)){
                    ctx.fillStyle = ‘#fff’;
                    ctx.beginPath(); ctx.arc(cx,cy,gridSize*0.35,0,Math.PI*2); ctx.fill();
                    drawSketchCircle(cx, cy, gridSize*0.35, ‘#111’, 1.5);
                }

                ctx.strokeStyle = ‘#111′; ctx.lineWidth = 2; ctx.lineCap = ’round’;
               
                if(type === ‘mon_skull’){
                    ctx.beginPath();
                    ctx.moveTo(cx-6, cy-4);
                    // Sketchy Cranium
                    ctx.quadraticCurveTo(cx, cy-14+rand*4, cx+6, cy-4);
                    // Cheekbones & Jaw
                    ctx.lineTo(cx+5, cy+2); ctx.lineTo(cx-5, cy+2);
                    ctx.closePath(); ctx.stroke();
                    // Jaw lines
                    drawSketchLine(cx-3, cy+2, cx-3, cy+6, ‘#111’, 1.5);
                    drawSketchLine(cx, cy+2, cx, cy+6, ‘#111’, 1.5);
                    drawSketchLine(cx+3, cy+2, cx+3, cy+6, ‘#111’, 1.5);
                    // Eyes
                    ctx.fillStyle=’#111′;
                    ctx.beginPath(); ctx.arc(cx-3, cy-3, 2, 0, 7); ctx.fill();
                    ctx.beginPath(); ctx.arc(cx+3, cy-3, 2, 0, 7); ctx.fill();
                }
                else if(type === ‘mon_slime’){
                    ctx.beginPath();
                    ctx.moveTo(cx-10, cy+8);
                    ctx.quadraticCurveTo(cx-12+rand*5, cy-12, cx, cy-14+rand*5);
                    ctx.quadraticCurveTo(cx+12-rand*5, cy-12, cx+10, cy+8);
                    ctx.closePath(); ctx.stroke();
                    // Bubble
                    ctx.beginPath(); ctx.arc(cx+4, cy-4, 2, 0, 7); ctx.stroke();
                }
                else if(type === ‘mon_eye’){
                    drawSketchCircle(cx, cy, 10, ‘#111’, 1.8); // Main eye
                    ctx.fillStyle=’#111′; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, 7); ctx.fill(); // Pupil
                    // Stalks
                    for(let i=0; i<5; i++){
                        const ang = (i/5)*Math.PI – Math.PI;
                        drawSketchLine(cx + Math.cos(ang)*10, cy + Math.sin(ang)*10, cx + Math.cos(ang)*16, cy + Math.sin(ang)*16, ‘#111’, 1.2);
                        ctx.beginPath(); ctx.arc(cx + Math.cos(ang)*18, cy + Math.sin(ang)*18, 1.5, 0, 7); ctx.fill();
                    }
                }
                else if(type === ‘mon_ghost’){
                    ctx.beginPath(); ctx.moveTo(cx-8, cy+8);
                    ctx.quadraticCurveTo(cx-8, cy-15, cx, cy-15);
                    ctx.quadraticCurveTo(cx+8, cy-15, cx+8, cy+8);
                    ctx.lineTo(cx+4, cy+4); ctx.lineTo(cx, cy+8); ctx.lineTo(cx-4, cy+4); ctx.closePath(); ctx.stroke();
                    ctx.fillStyle=’#111′; ctx.beginPath(); ctx.arc(cx-3, cy-4, 2, 0, 7); ctx.fill(); ctx.beginPath(); ctx.arc(cx+3, cy-4, 2, 0, 7); ctx.fill();
                }
                else if(type === ‘mon_boss’){
                    drawJaggedLine(cx-12, cy+5, cx+12, cy+5, ‘#900’, 2.5); // Mouth
                    drawSketchLine(cx-8, cy-5, cx-16, cy-12, ‘#111’, 2.5); // Horn L
                    drawSketchLine(cx+8, cy-5, cx+16, cy-12, ‘#111’, 2.5); // Horn R
                    ctx.beginPath(); ctx.arc(cx, cy, 10, 0, 7); ctx.stroke();
                    ctx.fillStyle=’#900′; ctx.beginPath(); ctx.arc(cx-4, cy-2, 2, 0, 7); ctx.fill(); ctx.beginPath(); ctx.arc(cx+4, cy-2, 2, 0, 7); ctx.fill();
                }
                else if(type === ‘chest’){
                    ctx.strokeRect(x+10, y+14, 20, 12);
                    drawSketchLine(x+10, y+20, x+30, y+20, ‘#111’, 1);
                    // Wood grain
                    drawSketchLine(x+14, y+16, x+26, y+16, ‘#555’, 0.8);
                    drawSketchLine(x+14, y+24, x+26, y+24, ‘#555’, 0.8);
                }
                else if(type === ‘treasure’){
                    for(let i=0; i<8; i++){
                        ctx.fillStyle=’#d97706′; ctx.beginPath();
                        ctx.arc(cx+(pseudoRandom(i,r)*20)-10, cy+(pseudoRandom(c,i)*20)-10, 2.5, 0, 7); ctx.fill();
                    }
                }
                else if(type === ‘trap’){
                    drawSketchLine(x+10, y+10, x+30, y+30, ‘#b91c1c’, 2.5);
                    drawSketchLine(x+30, y+10, x+10, y+30, ‘#b91c1c’, 2.5);
                }
                else if(type === ‘fountain’){
                    drawSketchCircle(cx, cy, 12, ‘#111’, 1.8);
                    drawSketchCircle(cx, cy, 6, ‘rgba(0,100,255,0.6)’, 1.5);
                }
                else if(type === ‘pillar’){
                    drawSketchCircle(cx, cy, 9, ‘#111’, 2.5);
                    // Cracks
                    if(rand > 0.5) drawSketchLine(cx-4, cy-4, cx+2, cy+2, ‘#555’, 1);
                }
                else if(type === ‘stairs’){
                    ctx.strokeRect(x+8, y+8, 24, 24);
                    for(let i=1; i<6; i++) drawSketchLine(x+8, y+8+(i*4), x+32, y+8+(i*4), ‘#111’, 1.5, 0);
                }

                ctx.restore(); // Undo rotation
            }
        }
    }

    function showSnapshot() {
        render();
        const img = document.getElementById(‘snapshot-img’);
        img.src = canvas.toDataURL(‘image/png’);
        document.getElementById(‘snapshot-overlay’).style.display = ‘flex’;
    }
   
    function hideSnapshot() { document.getElementById(‘snapshot-overlay’).style.display = ‘none’; }

    function downloadImage() {
        const link = document.createElement(‘a’);
        link.download = `dungeon_map_${Date.now()}.png`;
        link.href = canvas.toDataURL();
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    function openInNewTab() {
        const dataUrl = canvas.toDataURL();
        const win = window.open(‘about:blank’, ‘_blank’);
        if (win) {
            win.document.write(`<title>Dungeon Map</title><img src=”${dataUrl}” style=”width:100%”/>`);
            win.document.close();
        } else {
            alert(‘Pop-up blocked. Please check your browser settings.’);
        }
    }

    function getCoords(clientX, clientY) {
        const rect = viewport.getBoundingClientRect();
        const rx = (clientX-rect.left-panX)/zoom, ry = (clientY-rect.top-panY)/zoom;
        return { r: Math.floor(ry/gridSize), c: Math.floor(rx/gridSize), rx, ry };
    }

    function handleInput(e, force = false) {
        if (e.touches && e.touches.length > 1) return;
        const clientX = e.touches ? e.touches[0].clientX : e.clientX, clientY = e.touches ? e.touches[0].clientY : e.clientY;
        const { r, c, rx, ry } = getCoords(clientX, clientY);
        if (r < 0 || r >= rows || c < 0 || c >= cols) return;
        if (!force && r === lastCell.r && c === lastCell.c) return;
        lastCell = { r, c };

        if(currentMode === ‘room’) { if(force) this.dv = !rooms[r][c]; rooms[r][c] = this.dv; if(rooms[r][c]) flavor[r][c] = null; }
        else if([‘water’, ‘cavern’, ‘lava’].includes(currentMode)){ if(force) this.dv = flavor[r][c] === currentMode ? null : currentMode; flavor[r][c] = this.dv; if(flavor[r][c]) rooms[r][c] = false; }
        else if([‘door’, ‘thinwall’].includes(currentMode)){
            if(!force) return;
            const dx = rx-(c*gridSize), dy = ry-(r*gridSize);
            const ds = [{t:’h’,r,c,d:dy},{t:’h’,r:r+1,c,d:gridSize-dy},{t:’v’,r,c,d:dx},{t:’v’,r,c:c+1,d:gridSize-dx}].sort((a,b)=>a.d-b.d);
            const b = ds[0]; const target = currentMode===’door’?(b.t===’h’?hDoors:vDoors):(b.t===’h’?hThinWalls:vThinWalls);
            target[b.r][b.c] = !target[b.r][b.c];
        } else if(force) furniture[r][c] = (furniture[r][c] === currentMode) ? null : currentMode;
        render();
    }

    viewport.addEventListener(‘touchstart’, (e) => {
        if (e.touches.length === 2) {
            isDrawing = false; initialPinchDist = Math.hypot(e.touches[0].pageX – e.touches[1].pageX, e.touches[0].pageY – e.touches[1].pageY);
            initialZoom = zoom; this.lX = (e.touches[0].pageX + e.touches[1].pageX)/2; this.lY = (e.touches[0].pageY + e.touches[1].pageY)/2;
        } else { isDrawing = true; handleInput(e, true); }
    }, { passive: false });

    viewport.addEventListener(‘touchmove’, (e) => {
        e.preventDefault();
        if (e.touches.length === 2 && initialPinchDist) {
            const dist = Math.hypot(e.touches[0].pageX – e.touches[1].pageX, e.touches[0].pageY – e.touches[1].pageY);
            updateZoom(Math.max(0.2, Math.min(3, initialZoom * (dist / initialPinchDist))));
            const cx = (e.touches[0].pageX + e.touches[1].pageX)/2, cy = (e.touches[0].pageY + e.touches[1].pageY)/2;
            panX += cx – this.lX; panY += cy – this.lY; this.lX = cx; this.lY = cy; updateTransform();
        } else if (isDrawing) handleInput(e);
    }, { passive: false });

    viewport.addEventListener(‘touchend’, () => { isDrawing = false; lastCell = {r:-1, c:-1}; });
    viewport.addEventListener(‘mousedown’, (e) => { if(e.button===1 || (e.button===0 && e.altKey)) this.isPan = true; else { isDrawing=true; handleInput(e, true); } });
    window.addEventListener(‘mousemove’, (e) => { if(this.isPan){ panX += e.movementX; panY += e.movementY; updateTransform(); } else if(isDrawing) handleInput(e); });
    window.addEventListener(‘mouseup’, () => { this.isPan = false; isDrawing = false; lastCell = {r:-1, c:-1}; });
    viewport.addEventListener(‘wheel’, (e) => { e.preventDefault(); updateZoom(Math.max(0.2, Math.min(3, zoom * (e.deltaY>0?0.9:1.1)))); }, { passive: false });

    function clearMap() { if(!confirm(“Erase all progress?”)) return; rooms=rooms.map(r=>r.fill(false)); flavor=flavor.map(r=>r.fill(null)); furniture=furniture.map(r=>r.fill(null)); hDoors=hDoors.map(r=>r.fill(false)); vDoors=vDoors.map(r=>r.fill(false)); hThinWalls=hThinWalls.map(r=>r.fill(false)); vThinWalls=vThinWalls.map(r=>r.fill(false)); render(); }

    initCanvas();
</script>
</body>
</html>

Leave a Reply