Wilderness maps test

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
    <title>Advanced Wilderness Cartographer</title>
    <script src=”https://cdn.tailwindcss.com”></script>
    <style>
        @import url(‘https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=MedievalSharp&family=Quicksand:wght@300;500&display=swap’);

        :root {
            –bg-dark: #121212;
            –panel-bg: #1e1e1e;
            –accent: #d4af37; /* Gold */
            –accent-hover: #f1c40f;
        }

        body {
            background-color: var(–bg-dark);
            color: #e2e2e2;
            font-family: ‘Quicksand’, sans-serif;
            margin: 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            overflow-x: hidden;
        }

        h1, h2, h3, .map-font {
            font-family: ‘Cinzel’, serif;
        }

        .medieval-font {
            font-family: ‘MedievalSharp’, cursive;
        }

        #app-container {
            display: flex;
            flex-direction: column;
            width: 100%;
            max-width: 1400px;
            padding: 1rem;
            gap: 1.5rem;
        }

        @media (min-width: 1024px) {
            #app-container {
                flex-direction: row;
                align-items: flex-start;
            }
        }

        #canvas-wrapper {
            position: relative;
            flex-grow: 1;
            display: flex;
            justify-content: center;
            align-items: center;
            background: #0a0a0a;
            padding: 1rem;
            border-radius: 8px;
            border: 1px solid #333;
            box-shadow: inset 0 0 20px rgba(0,0,0,0.8);
            overflow: hidden;
        }

        canvas {
            max-width: 100%;
            height: auto;
            box-shadow: 0 10px 40px rgba(0,0,0,0.6);
            transition: transform 0.3s ease;
        }

        .controls-panel {
            background: var(–panel-bg);
            border: 1px solid #333;
            border-radius: 12px;
            padding: 1.5rem;
            width: 100%;
            min-width: 320px;
            max-width: 400px;
            flex-shrink: 0;
            box-shadow: 0 10px 30px rgba(0,0,0,0.5);
        }

        .control-group {
            margin-bottom: 1.2rem;
            padding-bottom: 1.2rem;
            border-bottom: 1px solid #2a2a2a;
        }
        .control-group:last-child {
            border-bottom: none;
            margin-bottom: 0;
            padding-bottom: 0;
        }

        input[type=range] {
            width: 100%;
            accent-color: var(–accent);
            height: 6px;
            background: #333;
            border-radius: 3px;
            outline: none;
            -webkit-appearance: none;
        }
        input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: var(–accent);
            cursor: pointer;
        }

        select, input[type=text] {
            width: 100%;
            background: #2a2a2a;
            border: 1px solid #444;
            color: white;
            padding: 0.5rem;
            border-radius: 6px;
            font-family: ‘Quicksand’, sans-serif;
        }

        select:focus, input[type=text]:focus {
            outline: none;
            border-color: var(–accent);
        }

        .btn {
            width: 100%;
            padding: 0.75rem;
            border-radius: 6px;
            font-family: ‘Cinzel’, serif;
            font-weight: bold;
            text-transform: uppercase;
            letter-spacing: 1px;
            transition: all 0.2s;
            cursor: pointer;
            border: none;
        }

        .btn-primary {
            background: var(–accent);
            color: #121212;
        }
        .btn-primary:hover {
            background: var(–accent-hover);
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3);
        }

        .btn-secondary {
            background: #333;
            color: #e2e2e2;
            border: 1px solid #555;
        }
        .btn-secondary:hover {
            background: #444;
        }

        .checkbox-container {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            margin-bottom: 0.5rem;
            cursor: pointer;
        }
        .checkbox-container input {
            accent-color: var(–accent);
            width: 16px;
            height: 16px;
        }
       
        /* Custom scrollbar for panel */
        .controls-panel::-webkit-scrollbar { width: 8px; }
        .controls-panel::-webkit-scrollbar-track { background: #1e1e1e; }
        .controls-panel::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
    </style>
</head>
<body>

    <div class=”text-center mt-6 mb-2 w-full”>
        <h1 class=”text-3xl md:text-5xl font-bold text-[#d4af37] tracking-widest drop-shadow-lg”>Cartographer’s Sanctum</h1>
        <p class=”text-gray-400 italic mt-2 text-sm md:text-base”>Advanced Procedural Atlas Generation</p>
    </div>

    <div id=”app-container”>
        <!– Sidebar Controls –>
        <div class=”controls-panel overflow-y-auto max-h-[85vh]”>
            <div class=”control-group”>
                <button id=”generateBtn” class=”btn btn-primary mb-3″>Forge New World</button>
                <div class=”flex gap-2″>
                    <input type=”text” id=”seedInput” placeholder=”Enter random seed…” class=”text-sm”>
                    <button id=”randomSeedBtn” class=”btn btn-secondary w-auto px-3″ title=”Random Seed”>🎲</button>
                </div>
            </div>

            <div class=”control-group”>
                <h3 class=”text-[#d4af37] text-sm tracking-widest mb-3″>Aesthetics</h3>
                <label class=”block text-xs text-gray-400 mb-1″>Map Theme</label>
                <select id=”themeSelect” class=”mb-3″>
                    <option value=”parchment”>Ancient Parchment (Classic)</option>
                    <option value=”atlas”>Imperial Atlas (Vibrant)</option>
                    <option value=”monochrome”>Scholar’s Ink (B&W)</option>
                    <option value=”dark”>Midnight Vellum (Dark Mode)</option>
                </select>
               
                <label class=”block text-xs text-gray-400 mb-1 mt-2″>Map Resolution</label>
                <select id=”resSelect” class=”mb-3″>
                    <option value=”1″>Standard (Faster)</option>
                    <option value=”2″>High (Detailed, Slower)</option>
                </select>
            </div>

            <div class=”control-group”>
                <h3 class=”text-[#d4af37] text-sm tracking-widest mb-3″>Geography (Whittaker Model)</h3>
               
                <div class=”mb-4″>
                    <div class=”flex justify-between text-xs text-gray-400 mb-1″>
                        <span>Landmass Scale</span>
                        <span id=”scaleVal”>150</span>
                    </div>
                    <input type=”range” id=”scale” min=”50″ max=”400″ value=”180″>
                </div>
               
                <div class=”mb-4″>
                    <div class=”flex justify-between text-xs text-gray-400 mb-1″>
                        <span>Sea Level</span>
                        <span id=”waterVal”>45%</span>
                    </div>
                    <input type=”range” id=”waterLevel” min=”20″ max=”80″ value=”45″>
                </div>

                <div class=”mb-2″>
                    <div class=”flex justify-between text-xs text-gray-400 mb-1″>
                        <span>Global Moisture</span>
                        <span id=”moistVal”>50%</span>
                    </div>
                    <input type=”range” id=”moisture” min=”-50″ max=”50″ value=”0″>
                </div>
            </div>

            <div class=”control-group”>
                <h3 class=”text-[#d4af37] text-sm tracking-widest mb-3″>Cartography Options</h3>
                <label class=”checkbox-container text-sm text-gray-300″>
                    <input type=”checkbox” id=”drawCoastlines” checked> Ink Coastlines
                </label>
                <label class=”checkbox-container text-sm text-gray-300″>
                    <input type=”checkbox” id=”drawSymbols” checked> Draw Terrain Symbols (Trees/Mts)
                </label>
                <label class=”checkbox-container text-sm text-gray-300″>
                    <input type=”checkbox” id=”drawPOIs” checked> Generate Cities & Ruins
                </label>
                <label class=”checkbox-container text-sm text-gray-300″>
                    <input type=”checkbox” id=”drawGrid”> Hexagonal Overlay
                </label>
                <label class=”checkbox-container text-sm text-gray-300″>
                    <input type=”checkbox” id=”drawDecor” checked> Compass & Title Ribbon
                </label>
            </div>

            <div class=”control-group”>
                <button id=”downloadBtn” class=”btn btn-secondary flex items-center justify-center gap-2″>
                    <svg class=”w-4 h-4″ fill=”none” stroke=”currentColor” viewBox=”0 0 24 24″><path stroke-linecap=”round” stroke-linejoin=”round” stroke-width=”2″ d=”M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4″></path></svg>
                    Export as PNG
                </button>
            </div>
        </div>

        <!– Canvas Area –>
        <div id=”canvas-wrapper”>
            <canvas id=”mapCanvas”></canvas>
        </div>
    </div>

    <script>
        /**
         * Robust seeded random number generator (Mulberry32)
         */
        function mulberry32(a) {
            return function() {
              var t = a += 0x6D2B79F5;
              t = Math.imul(t ^ t >>> 15, t | 1);
              t ^= t + Math.imul(t ^ t >>> 7, t | 61);
              return ((t ^ t >>> 14) >>> 0) / 4294967296;
            }
        }

        /**
         * Procedural Noise Implementation (Simplex-like Perlin)
         */
        class Noise {
            constructor(seedFunc) {
                this.p = new Uint8Array(512);
                const permutation = new Uint8Array(256);
                for (let i = 0; i < 256; i++) permutation[i] = i;
                for (let i = 255; i > 0; i–) {
                    const j = Math.floor(seedFunc() * (i + 1));
                    [permutation[i], permutation[j]] = [permutation[j], permutation[i]];
                }
                for (let i = 0; i < 512; i++) this.p[i] = permutation[i % 256];
            }
            fade(t) { return t * t * t * (t * (t * 6 – 15) + 10); }
            lerp(t, a, b) { return a + t * (b – a); }
            grad(hash, x, y) {
                const h = hash & 7;
                const u = h < 4 ? x : y;
                const v = h < 4 ? y : x;
                return ((h & 1) ? -u : u) + ((h & 2) ? -2.0*v : 2.0*v);
            }
            noise(x, y) {
                const X = Math.floor(x) & 255;
                const Y = Math.floor(y) & 255;
                x -= Math.floor(x);
                y -= Math.floor(y);
                const u = this.fade(x);
                const v = this.fade(y);
                const A = this.p[X] + Y, AA = this.p[A], AB = this.p[A + 1];
                const B = this.p[X + 1] + Y, BA = this.p[B], BB = this.p[B + 1];
                return this.lerp(v, this.lerp(u, this.grad(this.p[AA], x, y), this.grad(this.p[BA], x – 1, y)),
                                    this.lerp(u, this.grad(this.p[AB], x, y – 1), this.grad(this.p[BB], x – 1, y – 1)));
            }
            fbm(x, y, octaves = 4, persistence = 0.5, lacunarity = 2) {
                let total = 0;
                let frequency = 1;
                let amplitude = 1;
                let maxValue = 0;
                for(let i=0; i<octaves; i++) {
                    total += this.noise(x * frequency, y * frequency) * amplitude;
                    maxValue += amplitude;
                    amplitude *= persistence;
                    frequency *= lacunarity;
                }
                return total / maxValue;
            }
        }

        // — Application State & Configuration —
        const canvas = document.getElementById(‘mapCanvas’);
        const ctx = canvas.getContext(‘2d’, { alpha: false }); // Optimize
       
        let width = 1000;
        let height = 700;
        let rng; // Random Number Generator function
        let elevNoise, moistNoise;

        // Theme Definitions
        const THEMES = {
            parchment: {
                bg: ‘#f4e4bc’,
                ink: ‘#3d2b1f’,
                waterline: ‘rgba(61, 43, 31, 0.4)’,
                biomes: {
                    deepWater: ‘#d1bfae’, water: ‘#e0cdb8’, beach: ‘#eeddbd’,
                    desert: ‘#e8d2a6’, savanna: ‘#dfc999’, tropical: ‘#c5b68d’,
                    grassland: ‘#d4c79e’, forest: ‘#b3a886’, taiga: ‘#c4baa1’,
                    tundra: ‘#e2ddcd’, snow: ‘#f5f2eb’, mountain: ‘#b0a696’
                }
            },
            atlas: {
                bg: ‘#ffffff’,
                ink: ‘#1a1a1a’,
                waterline: ‘#2980b9’,
                biomes: {
                    deepWater: ‘#1c5c87’, water: ‘#3498db’, beach: ‘#f1c40f’,
                    desert: ‘#e67e22’, savanna: ‘#f39c12’, tropical: ‘#27ae60’,
                    grassland: ‘#2ecc71’, forest: ‘#229954’, taiga: ‘#145a32’,
                    tundra: ‘#95a5a6’, snow: ‘#ecf0f1’, mountain: ‘#7f8c8d’
                }
            },
            monochrome: {
                bg: ‘#ffffff’,
                ink: ‘#000000’,
                waterline: ‘#000000’,
                biomes: {
                    deepWater: ‘#e0e0e0’, water: ‘#ffffff’, beach: ‘#ffffff’,
                    desert: ‘#ffffff’, savanna: ‘#ffffff’, tropical: ‘#ffffff’,
                    grassland: ‘#ffffff’, forest: ‘#ffffff’, taiga: ‘#ffffff’,
                    tundra: ‘#ffffff’, snow: ‘#ffffff’, mountain: ‘#ffffff’ // Rely solely on symbols
                }
            },
            dark: {
                bg: ‘#121212’,
                ink: ‘#c8a951’, // Gold ink
                waterline: ‘#1f3a4d’,
                biomes: {
                    deepWater: ‘#0a1118’, water: ‘#121f2b’, beach: ‘#242018’,
                    desert: ‘#2e2213’, savanna: ‘#272614’, tropical: ‘#122617’,
                    grassland: ‘#1a2b1c’, forest: ‘#102116’, taiga: ‘#152424’,
                    tundra: ‘#2c3033’, snow: ‘#4a4e52’, mountain: ‘#2a2a2a’
                }
            }
        };

        let currentTheme = THEMES.parchment;

        // — Core Functions —

        function init() {
            // Setup Listeners
            document.getElementById(‘generateBtn’).addEventListener(‘click’, generateMap);
            document.getElementById(‘downloadBtn’).addEventListener(‘click’, downloadMap);
            document.getElementById(‘randomSeedBtn’).addEventListener(‘click’, () => {
                document.getElementById(‘seedInput’).value = Math.random().toString(36).substring(2, 8).toUpperCase();
            });
           
            // Sync slider values
            const sliders = [‘scale’, ‘waterLevel’, ‘moisture’];
            sliders.forEach(id => {
                const el = document.getElementById(id);
                el.addEventListener(‘input’, (e) => {
                    let val = e.target.value;
                    if(id === ‘waterLevel’) val += ‘%’;
                    if(id === ‘moisture’) val = (val > 0 ? ‘+’ : ”) + val + ‘%’;
                    document.getElementById(id.replace(‘Level’, ”).replace(‘ure’, ”) + ‘Val’).innerText = val;
                });
            });

            // Initial random seed
            document.getElementById(‘randomSeedBtn’).click();
           
            // First render
            generateMap();
        }

        function getBiome(e, m) {
            // e = elevation (0 to 1), m = moisture (0 to 1)
            const waterLevel = parseInt(document.getElementById(‘waterLevel’).value) / 100;
           
            if (e < waterLevel – 0.1) return ‘deepWater’;
            if (e < waterLevel) return ‘water’;
            if (e < waterLevel + 0.03) return ‘beach’;
           
            // High Elevation
            if (e > 0.8) return ‘snow’;
            if (e > 0.65) {
                if (m < 0.33) return ‘mountain’;
                if (m < 0.66) return ‘tundra’;
                return ‘snow’;
            }
           
            // Mid/Low Elevation
            if (m < 0.2) return ‘desert’;
            if (m < 0.4) return ‘savanna’;
            if (m < 0.65) return ‘grassland’;
            if (m < 0.85) return ‘forest’;
            if (e > waterLevel + 0.2) return ‘taiga’; // Cold wet
            return ‘tropical’; // Hot wet
        }

        // — Drawing Helpers —

        function setInk(alpha = 1, forceTheme = null) {
            const theme = forceTheme || currentTheme;
            if(alpha === 1) {
                ctx.strokeStyle = theme.ink;
                ctx.fillStyle = theme.ink;
            } else {
                // Convert hex to rgba manually for simplicity assuming hex is #RRGGBB
                let hex = theme.ink.replace(‘#’,”);
                if(hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
                let r = parseInt(hex.substring(0,2), 16);
                let g = parseInt(hex.substring(2,4), 16);
                let b = parseInt(hex.substring(4,6), 16);
                ctx.strokeStyle = `rgba(${r},${g},${b},${alpha})`;
                ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
            }
        }

        function drawMountain(x, y, size) {
            setInk(0.9);
            ctx.lineWidth = 1.5;
            ctx.beginPath();
            // Main peak
            ctx.moveTo(x – size, y + size*0.8);
            ctx.lineTo(x, y – size);
            ctx.lineTo(x + size, y + size*0.8);
            // Detail lines
            ctx.moveTo(x, y – size);
            ctx.lineTo(x – size*0.3, y + size*0.4);
            ctx.moveTo(x, y – size);
            ctx.lineTo(x + size*0.2, y + size*0.2);
            ctx.stroke();

            // Fill background so lines don’t show through
            ctx.fillStyle = currentTheme.biomes.mountain;
            ctx.fill();
            ctx.stroke();
        }

        function drawTree(x, y, size, type) {
            setInk(0.8);
            ctx.lineWidth = 1;
           
            if (type === ‘pine’ || type === ‘taiga’) {
                ctx.beginPath();
                ctx.moveTo(x, y + size);
                ctx.lineTo(x, y – size);
                ctx.moveTo(x, y – size);
                ctx.lineTo(x – size*0.6, y + size*0.3);
                ctx.moveTo(x, y – size);
                ctx.lineTo(x + size*0.6, y + size*0.3);
                ctx.stroke();
            } else if (type === ‘broadleaf’ || type === ‘forest’) {
                ctx.beginPath();
                ctx.arc(x, y – size*0.3, size*0.6, 0, Math.PI * 2);
                ctx.fillStyle = currentTheme.biomes.forest;
                if(document.getElementById(‘themeSelect’).value === ‘monochrome’) ctx.fillStyle = ‘#fff’;
                ctx.fill();
                ctx.stroke();
                // Trunk
                ctx.beginPath();
                ctx.moveTo(x, y + size*0.3);
                ctx.lineTo(x, y + size);
                ctx.stroke();
            } else if (type === ‘jungle’ || type === ‘tropical’) {
                 ctx.beginPath();
                 ctx.moveTo(x, y+size);
                 ctx.lineTo(x, y-size*0.5); // trunk
                 // Fronds
                 ctx.moveTo(x, y-size*0.5); ctx.quadraticCurveTo(x+size, y-size, x+size, y);
                 ctx.moveTo(x, y-size*0.5); ctx.quadraticCurveTo(x-size, y-size, x-size, y);
                 ctx.stroke();
            } else if (type === ‘cactus’ || type === ‘desert’) {
                ctx.beginPath();
                ctx.moveTo(x, y+size); ctx.lineTo(x, y-size*0.5); // main
                ctx.moveTo(x, y); ctx.lineTo(x+size*0.4, y); ctx.lineTo(x+size*0.4, y-size*0.8); // right arm
                ctx.moveTo(x, y+size*0.3); ctx.lineTo(x-size*0.4, y+size*0.3); ctx.lineTo(x-size*0.4, y-size*0.2); // left arm
                ctx.stroke();
            }
        }

        function drawCity(x, y, isCapital, name) {
            setInk(1);
            ctx.lineWidth = 1.5;
           
            if (isCapital) {
                // Draw Star/Castle
                ctx.beginPath();
                ctx.arc(x, y, 6, 0, Math.PI * 2);
                ctx.fillStyle = currentTheme.ink;
                ctx.fill();
                ctx.beginPath();
                ctx.arc(x, y, 9, 0, Math.PI * 2);
                ctx.stroke();
            } else {
                // Small dot/house
                ctx.beginPath();
                ctx.arc(x, y, 4, 0, Math.PI * 2);
                ctx.fillStyle = currentTheme.ink;
                ctx.fill();
            }

            // Name
            ctx.font = “bold 14px ‘MedievalSharp’, cursive”;
            ctx.fillStyle = currentTheme.ink;
            ctx.textAlign = “center”;
            // Outline text for readability
            ctx.strokeStyle = currentTheme.bg;
            ctx.lineWidth = 3;
            ctx.strokeText(name, x, y – 12);
            ctx.fillText(name, x, y – 12);
        }

        function drawCompass(x, y, size) {
            setInk(1);
            ctx.lineWidth = 2;
           
            // Outer Ring
            ctx.beginPath();
            ctx.arc(x, y, size, 0, Math.PI * 2);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(x, y, size * 0.9, 0, Math.PI * 2);
            ctx.stroke();

            // Points
            const drawPoint = (angle, len, width, fillDark) => {
                ctx.save();
                ctx.translate(x, y);
                ctx.rotate(angle);
                ctx.beginPath();
                ctx.moveTo(0, -len);
                ctx.lineTo(width, 0);
                ctx.lineTo(0, 0);
                ctx.fillStyle = fillDark ? currentTheme.ink : currentTheme.bg;
                ctx.fill();
                ctx.stroke();

                ctx.beginPath();
                ctx.moveTo(0, -len);
                ctx.lineTo(-width, 0);
                ctx.lineTo(0, 0);
                ctx.fillStyle = fillDark ? currentTheme.bg : currentTheme.ink;
                ctx.fill();
                ctx.stroke();
                ctx.restore();
            };

            // Cardinal
            for(let i=0; i<4; i++) drawPoint(i * Math.PI/2, size * 1.2, size * 0.25, true);
            // Ordinal
            for(let i=0; i<4; i++) drawPoint(i * Math.PI/2 + Math.PI/4, size * 0.8, size * 0.15, false);

            // N label
            ctx.font = “bold 24px ‘Cinzel’, serif”;
            ctx.fillStyle = currentTheme.ink;
            ctx.textAlign = “center”;
            ctx.textBaseline = “middle”;
            ctx.fillText(“N”, x, y – size * 1.5);
        }

        function drawTitleRibbon(text, x, y) {
            const width = ctx.measureText(text).width + 80;
            const height = 50;
            setInk(1);
           
            // Background Ribbon
            ctx.fillStyle = currentTheme.bg;
            ctx.strokeStyle = currentTheme.ink;
            ctx.lineWidth = 2;

            // Main body
            ctx.fillRect(x – width/2, y – height/2, width, height);
            ctx.strokeRect(x – width/2, y – height/2, width, height);
           
            // Tails
            const tailW = 30;
            ctx.beginPath();
            ctx.moveTo(x – width/2, y – height/2 + 10);
            ctx.lineTo(x – width/2 – tailW, y – height/2 + 10);
            ctx.lineTo(x – width/2 – tailW/2, y);
            ctx.lineTo(x – width/2 – tailW, y + height/2 – 10);
            ctx.lineTo(x – width/2, y + height/2 – 10);
            ctx.fill(); ctx.stroke();

            ctx.beginPath();
            ctx.moveTo(x + width/2, y – height/2 + 10);
            ctx.lineTo(x + width/2 + tailW, y – height/2 + 10);
            ctx.lineTo(x + width/2 + tailW/2, y);
            ctx.lineTo(x + width/2 + tailW, y + height/2 – 10);
            ctx.lineTo(x + width/2, y + height/2 – 10);
            ctx.fill(); ctx.stroke();

            // Text
            ctx.font = “bold 28px ‘Cinzel’, serif”;
            ctx.fillStyle = currentTheme.ink;
            ctx.textAlign = “center”;
            ctx.textBaseline = “middle”;
            ctx.fillText(text, x, y);
        }

        // — Name Generation —
        const PRE = [“Aethel”, “Ald”, “Bal”, “Cor”, “Dun”, “Ea”, “Fael”, “Gor”, “Hal”, “Ili”, “Kel”, “Lor”, “Mor”, “Nor”, “Oth”, “Pel”, “Qar”, “Riv”, “Sil”, “Tor”, “Ul”, “Val”, “Win”, “Xyl”, “Yar”, “Zar”];
        const SUF = [“gard”, “ia”, “ton”, “berg”, “grad”, “vale”, “dor”, “mar”, “thal”, “wyn”, “ford”, “keep”, “run”, “fall”, “wood”, “sea”, “peak”, “fast”];
        function generateName() {
            const p = PRE[Math.floor(rng() * PRE.length)];
            const s = SUF[Math.floor(rng() * SUF.length)];
            return p + s;
        }

        // — Main Generation Routine —

        function generateMap() {
            // Setup Canvas & Resolution
            const resMultiplier = parseInt(document.getElementById(‘resSelect’).value);
            width = 1000 * resMultiplier;
            height = 700 * resMultiplier;
            canvas.width = width;
            canvas.height = height;

            // Get Settings
            const seedStr = document.getElementById(‘seedInput’).value || ‘MAP’;
            // Convert string seed to integer
            let seedNum = 0;
            for(let i=0; i<seedStr.length; i++) seedNum += seedStr.charCodeAt(i) * Math.pow(10, i);
           
            rng = mulberry32(seedNum);
            elevNoise = new Noise(rng);
            moistNoise = new Noise(rng);

            const scale = parseInt(document.getElementById(‘scale’).value) * resMultiplier;
            const waterLevel = parseInt(document.getElementById(‘waterLevel’).value) / 100;
            const globalMoisture = parseInt(document.getElementById(‘moisture’).value) / 100;
           
            currentTheme = THEMES[document.getElementById(‘themeSelect’).value];
            const doCoastlines = document.getElementById(‘drawCoastlines’).checked;
            const doSymbols = document.getElementById(‘drawSymbols’).checked;
            const doPOIs = document.getElementById(‘drawPOIs’).checked;
            const doGrid = document.getElementById(‘drawGrid’).checked;
            const doDecor = document.getElementById(‘drawDecor’).checked;

            const gridScale = resMultiplier > 1 ? 2 : 3; // Pixel size. Smaller = finer detail

            // 1. Generate Maps & Draw Base Biomes
            ctx.fillStyle = currentTheme.bg;
            ctx.fillRect(0, 0, width, height);

            const mapData = []; // Store data for passes

            for (let x = 0; x < width; x += gridScale) {
                mapData[x] = [];
                for (let y = 0; y < height; y += gridScale) {
                   
                    // Elevation
                    let nx = x / scale;
                    let ny = y / scale;
                    // Distance from center for island generation (Vignette)
                    const dx = x/width – 0.5;
                    const dy = y/height – 0.5;
                    const dist = Math.sqrt(dx*dx + dy*dy) * 2;
                   
                    // Elevation noise (fbm) normalized to -1 to 1
                    let e = elevNoise.fbm(nx, ny, 6, 0.5, 2);
                    // Map to 0-1 and apply circular mask
                    e = (e + 1) / 2;
                    e = e – (dist * 0.4); // Force edges to be water
                    if(e < 0) e = 0;

                    // Moisture
                    let m = moistNoise.fbm(nx + 100, ny + 100, 4, 0.5, 2);
                    m = (m + 1) / 2;
                    m += globalMoisture; // Apply user offset
                    if(m < 0) m = 0; if(m > 1) m = 1;

                    const biome = getBiome(e, m);
                    mapData[x][y] = { e, m, biome };

                    // Only draw biomes if not purely monochrome, or if water in monochrome
                    if (document.getElementById(‘themeSelect’).value === ‘monochrome’) {
                        if(biome.includes(‘water’) || biome === ‘beach’) {
                           // Stipple effect for monochrome water
                           if(rng() > 0.95 && biome !== ‘deepWater’) {
                               ctx.fillStyle = ‘#000’;
                               ctx.fillRect(x, y, 1, 1);
                           }
                        }
                    } else {
                        ctx.fillStyle = currentTheme.biomes[biome];
                        ctx.fillRect(x, y, gridScale, gridScale);
                    }
                }
            }

            // 2. Texture Overlay
            if(document.getElementById(‘themeSelect’).value !== ‘monochrome’) {
                ctx.globalAlpha = 0.05;
                for (let i = 0; i < 10000 * resMultiplier; i++) {
                    ctx.fillStyle = rng() > 0.5 ? ‘#000’ : ‘#fff’;
                    ctx.fillRect(rng() * width, rng() * height, 2, 2);
                }
                ctx.globalAlpha = 1.0;
            }

            // 3. Coastlines (Ink Outline)
            if (doCoastlines) {
                ctx.strokeStyle = currentTheme.waterline;
                ctx.lineWidth = 1 * resMultiplier;
                ctx.beginPath();
                for (let x = gridScale; x < width – gridScale; x += gridScale) {
                    for (let y = gridScale; y < height – gridScale; y += gridScale) {
                        const current = mapData[x][y].biome;
                        const isLand = !current.includes(‘water’);
                        if(isLand) {
                            // Check neighbors
                            if(mapData[x+gridScale] && mapData[x+gridScale][y] && mapData[x+gridScale][y].biome.includes(‘water’)) {
                                ctx.moveTo(x+gridScale, y); ctx.lineTo(x+gridScale, y+gridScale);
                            }
                            if(mapData[x-gridScale] && mapData[x-gridScale][y] && mapData[x-gridScale][y].biome.includes(‘water’)) {
                                ctx.moveTo(x, y); ctx.lineTo(x, y+gridScale);
                            }
                            if(mapData[x] && mapData[x][y+gridScale] && mapData[x][y+gridScale].biome.includes(‘water’)) {
                                ctx.moveTo(x, y+gridScale); ctx.lineTo(x+gridScale, y+gridScale);
                            }
                            if(mapData[x] && mapData[x][y-gridScale] && mapData[x][y-gridScale].biome.includes(‘water’)) {
                                ctx.moveTo(x, y); ctx.lineTo(x+gridScale, y);
                            }
                        }
                    }
                }
                ctx.stroke();

                // Inner and outer ripple lines
                ctx.globalAlpha = 0.3;
                ctx.lineWidth = 0.5 * resMultiplier;
                ctx.stroke();
                ctx.globalAlpha = 1.0;
            }

            // 4. Hex Grid Overlay
            if (doGrid) {
                setInk(0.15);
                ctx.lineWidth = 1;
                const hexSize = 25 * resMultiplier;
                for (let x = 0; x < width + hexSize; x += hexSize * 1.5) {
                    for (let y = 0; y < height + hexSize; y += hexSize * Math.sqrt(3)) {
                        ctx.beginPath();
                        for (let i = 0; i < 6; i++) {
                            const angle = 2 * Math.PI / 6 * i;
                            const x_i = x + hexSize * Math.cos(angle);
                            const y_i = (y + (x / (hexSize * 1.5) % 2 === 0 ? 0 : hexSize * Math.sqrt(3)/2)) + hexSize * Math.sin(angle);
                            if (i === 0) ctx.moveTo(x_i, y_i);
                            else ctx.lineTo(x_i, y_i);
                        }
                        ctx.closePath();
                        ctx.stroke();
                    }
                }
            }

            // 5. Symbols (Mountains, Trees) & Gather POI locations
            const validPOILocations = [];
           
            if (doSymbols) {
                const step = 15 * resMultiplier; // Spacing between checks
               
                // Need to draw top to bottom for correct z-sorting
                for (let y = 0; y < height; y += step) {
                    for (let x = 0; x < width; x += step) {
                        // Snap to data grid
                        const gx = Math.floor(x / gridScale) * gridScale;
                        const gy = Math.floor(y / gridScale) * gridScale;
                       
                        if(!mapData[gx] || !mapData[gx][gy]) continue;
                       
                        const data = mapData[gx][gy];
                        const b = data.biome;
                       
                        const jX = x + (rng() – 0.5) * step * 0.8;
                        const jY = y + (rng() – 0.5) * step * 0.8;
                        const size = (6 + rng() * 4) * resMultiplier;

                        if (b === ‘mountain’ || b === ‘snow’) {
                            if(rng() > 0.3) drawMountain(jX, jY, size * 1.5);
                        } else if (b === ‘forest’ || b === ‘taiga’ || b === ‘tropical’) {
                            if(rng() > 0.4) drawTree(jX, jY, size, b);
                        } else if (b === ‘desert’ || b === ‘savanna’) {
                            if(rng() > 0.9) drawTree(jX, jY, size * 0.8, ‘cactus’);
                        }

                        // Collect POI candidates (Flat land, not water, not mountain)
                        if(!b.includes(‘water’) && b !== ‘mountain’ && b !== ‘snow’) {
                            if(rng() > 0.95) { // Thin out candidates
                                validPOILocations.push({x: jX, y: jY, biome: b});
                            }
                        }
                    }
                }
            }

            // 6. Points of Interest (Cities)
            if (doPOIs && validPOILocations.length > 0) {
                // Shuffle candidates
                for (let i = validPOILocations.length – 1; i > 0; i–) {
                    const j = Math.floor(rng() * (i + 1));
                    [validPOILocations[i], validPOILocations[j]] = [validPOILocations[j], validPOILocations[i]];
                }

                const numCities = Math.min(Math.floor(rng() * 6) + 3, validPOILocations.length); // 3 to 8 cities
                const placed = [];

                for (let i = 0; i < numCities; i++) {
                    const loc = validPOILocations[i];
                   
                    // Simple distance check to prevent overlapping text
                    let tooClose = false;
                    for(let p of placed) {
                        const dist = Math.hypot(p.x – loc.x, p.y – loc.y);
                        if(dist < 80 * resMultiplier) tooClose = true;
                    }
                    if(tooClose) continue;

                    const isCap = placed.length === 0; // First one is capital
                    drawCity(loc.x, loc.y, isCap, generateName());
                    placed.push(loc);
                }
            }

            // 7. Decorations (Border, Compass, Title)
           
            // Frame/Vignette
            const grad = ctx.createRadialGradient(width/2, height/2, width*0.3, width/2, height/2, width*0.7);
            grad.addColorStop(0, ‘rgba(0,0,0,0)’);
            grad.addColorStop(1, document.getElementById(‘themeSelect’).value === ‘dark’ ? ‘rgba(0,0,0,0.8)’ : ‘rgba(0,0,0,0.3)’);
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, width, height);

            // Border
            setInk(1);
            ctx.lineWidth = 4 * resMultiplier;
            ctx.strokeRect(10 * resMultiplier, 10 * resMultiplier, width – 20*resMultiplier, height – 20*resMultiplier);
            ctx.lineWidth = 1 * resMultiplier;
            ctx.strokeRect(16 * resMultiplier, 16 * resMultiplier, width – 32*resMultiplier, height – 32*resMultiplier);

            if (doDecor) {
                drawCompass(width – 90 * resMultiplier, height – 90 * resMultiplier, 40 * resMultiplier);
               
                // Generate World Name
                const title = `The Realm of ${generateName()}`;
                drawTitleRibbon(title, width/2, 60 * resMultiplier);
            }
        }

        function downloadMap() {
            const link = document.createElement(‘a’);
            link.download = `Map_${document.getElementById(‘seedInput’).value}.png`;
            link.href = canvas.toDataURL(“image/png”);
            link.click();
        }

        // Run
        window.onload = init;

    </script>
</body>
</html>

Leave a Reply