Virtual Fantasy Kingdom Map Maker

Design Your Realm

Basic Settings
Select Key Features

Loading features...

No features defined.

"} featuresCheckboxesEl.innerHTML = checkboxesHTML; } // Simplified Placement Logic const placedFeaturesCoords = []; const mapPadding = 8; const minIconDistance = 6; // % function getPlacementCoords(featureKey, zoneHints = ['any']) { const mapW = 100; const mapH = 100; let x, y, validPlacement = false; let attempts = 0; const maxAttempts = 50; while (!validPlacement && attempts < maxAttempts) { attempts++; let minX = mapPadding, maxX = mapW - mapPadding, minY = mapPadding, maxY = mapH - mapPadding; const currentZoneHints = (Array.isArray(zoneHints) && zoneHints.length > 0) ? zoneHints : ['any']; // Ensure valid zones array const zone = getRandomElement(currentZoneHints); // Zone definitions... (same as before, omitted for brevity) if (zone === 'north') { maxY = 35; } else if (zone === 'south') { minY = 65; } else if (zone === 'west') { maxX = 35; } else if (zone === 'east') { minX = 65; } else if (zone === 'center') { minX = 30; maxX = 70; minY = 30; maxY = 70; } else if (zone === 'north_west') { maxX = 40; maxY = 40; } else if (zone === 'north_east') { minX = 60; maxY = 40; } else if (zone === 'south_west') { maxX = 40; minY = 60; } else if (zone === 'south_east') { minX = 60; minY = 60; } else if (zone === 'coast' || zone === 'coast_center') { const edge = getRandomInt(1, 4); if (edge === 1) maxY = mapPadding + 15; else if (edge === 2) minX = mapW - mapPadding - 15; else if (edge === 3) minY = mapH - mapPadding - 15; else if (edge === 4) maxX = mapPadding + 15; if(zone === 'coast_center'){ if(edge===1 || edge===3) {minX=30; maxX=70;} else {minY=30;maxY=70;} } } else if (zone === 'mountains') { maxY = 30; } else if (zone === 'hills') { minY=25; maxY=75; minX=25; maxX=75; } else if (zone === 'border') { const edge = getRandomInt(1, 4); if (edge === 1) {y = mapPadding; x=getRandomInt(minX, maxX);} else if (edge === 2) {x = mapW - mapPadding; y=getRandomInt(minY, maxY);} else if (edge === 3) {y = mapH - mapPadding; x=getRandomInt(minX, maxX);} else {x = mapPadding; y=getRandomInt(minY, maxY);} } else if (zone === 'remote') { const corner = getRandomInt(1,4); if (corner === 1) {maxX=25; maxY=25;} else if (corner === 2) {minX=75; maxY=25;} else if (corner === 3) {minX=75; minY=75;} else {maxX=25; minY=75;} } if (zone !== 'border') { x = getRandomInt(minX, maxX); y = getRandomInt(minY, maxY); } validPlacement = true; for (const placed of placedFeaturesCoords) { const dist = Math.sqrt(Math.pow(x - placed.x, 2) + Math.pow(y - placed.y, 2)); if (dist < minIconDistance) { validPlacement = false; break; } } } if (attempts >= maxAttempts) console.warn(`Max placement attempts reached for ${featureKey}, potential overlap.`); placedFeaturesCoords.push({ x, y, size: 5 }); return { top: y, left: x }; } // Generate River Path data function generateRiverPath(startZone = 'mountains') { let startX = getRandomInt(20, 80), startY = getRandomInt(5, 15), endX = getRandomInt(20, 80), endY = 100; if (startZone === 'mountains') startY = getRandomInt(15, 30); else if (startZone === 'hills') startY = getRandomInt(25, 45); const midXVariance = 35; const midYVariance = 25; const cp1X = startX + getRandomInt(-midXVariance, midXVariance); const cp1Y = startY + (endY - startY) * 0.33 + getRandomInt(-midYVariance, midYVariance); const cp2X = endX + getRandomInt(-midXVariance, midXVariance); const cp2Y = startY + (endY - startY) * 0.66 + getRandomInt(-midYVariance, midYVariance); const clamp = (val) => Math.max(0, Math.min(100, val)); return `M ${clamp(startX)} ${clamp(startY)} C ${clamp(cp1X)} ${clamp(cp1Y)}, ${clamp(cp2X)} ${clamp(cp2Y)}, ${clamp(endX)} ${clamp(endY)}`; } // --- Main Generation Logic (With added checks) --- function generateMap() { const genId = Date.now(); console.log(`/* DEBUG ${genId}: */ generateMap started.`); errorMsgDiv.textContent = ''; placedFeaturesCoords.length = 0; if(generateBtn) { generateBtn.disabled = true; generateBtn.textContent = 'Generating...'; generateBtn.style.opacity = '0.7'; } setTimeout(() => { console.log(`/* DEBUG ${genId}: */ Starting generation logic inside setTimeout.`); // --- Add Master Try/Catch/Finally --- try { // --- Get Inputs (with check for checkbox container) --- console.log(`/* DEBUG ${genId}: */ Reading inputs...`); if (!featuresCheckboxesEl) throw new Error("Features checkbox container not found during generation."); const terrain = document.getElementById('mapTerrain').value; const size = document.getElementById('mapSize').value; const style = document.getElementById('mapStyle').value; const neighbor1 = sanitizeInput(document.getElementById('neighbor1').value.trim()); const neighbor2 = sanitizeInput(document.getElementById('neighbor2').value.trim()); const selectedFeaturesCheckboxes = featuresCheckboxesEl.querySelectorAll('input[type="checkbox"]:checked'); const selectedFeatureKeys = Array.from(selectedFeaturesCheckboxes).map(cb => cb.value); console.log(`/* DEBUG ${genId}: */ Inputs - Terrain: ${terrain}, Size: ${size}, Style: ${style}, Features Selected: ${selectedFeatureKeys.length}`); // --- Generate Kingdom Name (with check for output element) --- console.log(`/* DEBUG ${genId}: */ Generating kingdom name...`); if (!kingdomNameOutputEl) throw new Error("Kingdom Name output element not found."); const kingdomName = generateName(nameParts); kingdomNameOutputEl.textContent = `${kingdomName} - Kingdom Map`; console.log(`/* DEBUG ${genId}: */ Kingdom Name: ${kingdomName}`); // --- Apply Style/Terrain (with check for map area) --- console.log(`/* DEBUG ${genId}: */ Applying style: style-${style.toLowerCase()}`); if (!mapArea) throw new Error("Map Area element not found."); mapArea.className = `style-${style.toLowerCase()}`; // --- Determine Feature Counts --- console.log(`/* DEBUG ${genId}: */ Determining feature counts for size: ${size}`); let maxFeatures, minFeatures, maxVillages, maxHills, maxForests; switch(size) { case 'Small': minFeatures=4; maxFeatures=6; maxVillages=2; maxHills=3; maxForests=2; break; case 'Medium': minFeatures=7; maxFeatures=10; maxVillages=4; maxHills=5; maxForests=3; break; case 'Large': minFeatures=10; maxFeatures=15; maxVillages=6; maxHills=7; maxForests=4; break; case 'Huge': minFeatures=15; maxFeatures=22; maxVillages=8; maxHills=9; maxForests=5; break; default: minFeatures=7; maxFeatures=10; maxVillages=4; maxHills=5; maxForests=3; } const targetFeatureCount = getRandomInt(minFeatures, maxFeatures); console.log(`/* DEBUG ${genId}: */ Target feature count: ${targetFeatureCount}`); // --- Build Feature List (robust checks) --- console.log(`/* DEBUG ${genId}: */ Building feature list...`); const featuresToPlace = []; const placedKeys = new Set(); selectedFeatureKeys.forEach(key => { const featureData = featuresData[key]; if (featureData) { if (!featureData.allowMultiple && placedKeys.has(key)) { console.warn(`/* WARN ${genId}: */ Skipping duplicate non-multiple feature: ${key}`); return; } featuresToPlace.push(key); placedKeys.add(key); console.log(`/* DEBUG ${genId}: */ Added selected feature: ${key}`); } else { console.warn(`/* WARN ${genId}: */ Selected feature key '${key}' not found in featuresData.`); } }); let currentVillages = featuresToPlace.filter(k => k==='village').length; let currentHills = featuresToPlace.filter(k => k==='hills').length; let currentForests = featuresToPlace.filter(k => k==='forest_minor').length; const fillerTypes = []; for(let i=0; i 0) { const fillerIndex = getRandomInt(0, fillerTypes.length - 1); const fillerKey = fillerTypes.splice(fillerIndex, 1)[0]; if (featuresData[fillerKey]) { if (!featuresData[fillerKey].allowMultiple && placedKeys.has(fillerKey)) continue; featuresToPlace.push(fillerKey); placedKeys.add(fillerKey); } } // Check allowMultiple for fillers too console.log(`/* DEBUG ${genId}: */ Final features to place (${featuresToPlace.length}): ${featuresToPlace.join(', ')}`); // --- Generate Feature Names & Determine River Source --- console.log(`/* DEBUG ${genId}: */ Generating names and finding river source...`); let riverSourceZone = featuresToPlace.includes('mountains') ? 'mountains' : 'hills'; let riverName = ''; let hasRiverSourceFeature = false; featuresToPlace.forEach(key => { const feature = featuresData[key]; if (feature?.needsName) { feature.generatedName = generatePlaceName(placeNameParts); console.log(`/* DEBUG ${genId}: */ Generated name for ${key}: ${feature.generatedName}`); } if (key === 'river_source') { hasRiverSourceFeature = true; riverSourceZone = getRandomElement(feature.zones || ['mountains']); riverName = feature.generatedName || generatePlaceName(placeNameParts) + " River"; console.log(`/* DEBUG ${genId}: */ Found river_source feature. Zone: ${riverSourceZone}, Name: ${riverName}`); } }); if (!riverName && featuresToPlace.some(k => ['mountains', 'hills', 'lake'].includes(k))) { riverName = generatePlaceName(placeNameParts) + " River"; console.log(`/* DEBUG ${genId}: */ Implied river source, generated name: ${riverName}`);} // --- Generate Feature HTML & Placement (robust checks) --- console.log(`/* DEBUG ${genId}: */ Starting feature placement loop...`); let mapFeaturesHTML = ''; featuresToPlace.forEach((key, index) => { console.log(`/* DEBUG ${genId}: */ Processing feature ${index + 1}/${featuresToPlace.length}: ${key}`); const feature = featuresData[key]; // *** Robustness Check *** if (!feature) { console.warn(`/* WARN ${genId}: */ Feature data not found for key: ${key}. Skipping.`); return; } if (key === 'river_source') { console.log(`/* DEBUG ${genId}: */ Placing label for ${riverName || 'River Source'}`); mapFeaturesHTML += `
${riverName || 'River Source'}
`; return; } if (!feature.icon) { console.warn(`/* WARN ${genId}: */ Icon missing for feature key: ${key}. Skipping.`); return; } const name = feature.generatedName || ''; const coords = getPlacementCoords(key, feature.zones); // Assuming getPlacementCoords handles bad zones if (!coords) { console.warn(`/* WARN ${genId}: */ Failed to get placement coords for ${key}. Skipping.`); return;} const iconColorVar = feature.iconColorVar || '--icon-color'; const labelColorVar = feature.labelColorVar || '--feature-color'; mapFeaturesHTML += `
${feature.icon}
${name ? `${name}` : ''}
\n`; }); // --- Generate River --- if (riverName) { console.log(`/* DEBUG ${genId}: */ Generating river path starting near ${riverSourceZone}...`); try { const riverPathData = generateRiverPath(riverSourceZone); console.log(`/* DEBUG ${genId}: */ River path data generated.`); const riverHTML = ``; mapFeaturesHTML += riverHTML; } catch (riverError) { console.error(`/* DEBUG ${genId}: */ Error generating river path:`, riverError); errorMsgDiv.textContent += " Error generating river. "; } } else { console.log(`/* DEBUG ${genId}: */ No river generated.`); } // --- Add Neighbor Labels --- if (neighbor1) { const pos = getRandomElement(['top', 'left']); mapFeaturesHTML += `
${neighbor1}
`; } if (neighbor2) { const pos = getRandomElement(['bottom', 'right']); mapFeaturesHTML += `
${neighbor2}
`; } // --- Update Map Area --- console.log(`/* DEBUG ${genId}: */ Updating mapArea innerHTML...`); if (!mapArea) throw new Error("Map Area element became null before final update."); mapArea.innerHTML = mapFeaturesHTML; // --- Show Output --- console.log(`/* DEBUG ${genId}: */ Making output visible...`); if (!outputContainer) throw new Error("Output Container element not found when showing output."); outputContainer.style.display = 'block'; if (downloadBtn) { downloadBtn.style.display = 'block'; jsPDFLoaded = (typeof window.jspdf !== 'undefined'); html2canvasLoaded = (typeof window.html2canvas !== 'undefined'); downloadBtn.disabled = !jsPDFLoaded || !html2canvasLoaded; } else { console.warn(`/* WARN ${genId}: */ Download button not found when trying to show output.`); } console.log(`/* DEBUG ${genId}: */ Map generated and displayed successfully.`); } catch (error) { // Catch errors from the main generation logic console.error(`/* DEBUG ${genId}: */ ERROR caught during map generation:`, error); errorMsgDiv.textContent = `Map Generation Error: ${error.message}. Check console for details.`; if (outputContainer) outputContainer.style.display = 'none'; // Hide potentially broken output } finally { // --- Finally block to ensure button reset --- console.log(`/* DEBUG ${genId}: */ Running finally block for map generation.`); if(generateBtn) { generateBtn.disabled = false; generateBtn.textContent = 'Generate Map'; generateBtn.style.opacity = '1'; } else { console.warn(`/* DEBUG ${genId}: */ Generate button not found in finally block.`); } } }, 10); // setTimeout delay } // --- PDF Download Logic (Using html2canvas - Corrected Version from previous step) --- function downloadPDF() { const uniqueId = Date.now(); console.log(`/* DEBUG ${uniqueId}: */ PDF Download button clicked.`); errorMsgDiv.textContent = ''; if(downloadBtn) { downloadBtn.disabled = true; downloadBtn.textContent = 'Processing...'; downloadBtn.style.opacity = '0.7'; } setTimeout(() => { console.log(`/* DEBUG ${uniqueId}: */ Starting PDF processing inside setTimeout.`); // --- Robust Library Check --- jsPDFLoaded = (typeof window.jspdf !== 'undefined' && typeof window.jspdf.jsPDF !== 'undefined'); html2canvasLoaded = (typeof window.html2canvas !== 'undefined'); if (!jsPDFLoaded) { console.error(`/* DEBUG ${uniqueId}: */ jsPDF library check failed.`); errorMsgDiv.textContent = "Error: jsPDF library not available."; if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } return; } if (!html2canvasLoaded) { console.error(`/* DEBUG ${uniqueId}: */ html2canvas library check failed.`); errorMsgDiv.textContent = "Error: html2canvas library not available for image capture."; if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } return; } // --- End Library Check --- const mapAreaElement = document.getElementById('mapArea'); if (!mapAreaElement || !mapAreaElement.innerHTML.trim()) { console.error(`/* DEBUG ${uniqueId}: */ No content found in #mapArea.`); errorMsgDiv.textContent = "Error: Please generate a map first."; if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } return; } const kingdomNameEl = document.getElementById('kingdomNameOutput'); const kingdomName = kingdomNameEl ? kingdomNameEl.textContent.replace(' - Kingdom Map','') : 'Fantasy-Map'; const pdfFilename = `${kingdomName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_map.pdf`; console.log(`/* DEBUG ${uniqueId}: */ PDF Filename: ${pdfFilename}`); console.log(`/* DEBUG ${uniqueId}: */ Attempting PDF generation using METHOD 2: html2canvas...`); try { const targetElement = document.getElementById('mapArea'); html2canvas(targetElement, { scale: 2.5, useCORS: true, backgroundColor: window.getComputedStyle(targetElement).backgroundColor || '#f5eabc', logging: false, width: targetElement.offsetWidth, height: targetElement.offsetHeight, scrollX: 0, scrollY: 0 }) .then(canvas => { console.log(`/* DEBUG ${uniqueId}: */ html2canvas capture successful.`); if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { console.error(`/* DEBUG ${uniqueId}: */ jsPDF missing inside html2canvas.then!`); errorMsgDiv.textContent = "Error: PDF Library component missing after capture."; if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } return; } const { jsPDF } = window.jspdf; const imgData = canvas.toDataURL('image/png'); const imgProps = { width: canvas.width, height: canvas.height }; const orientation = imgProps.width >= imgProps.height ? 'l' : 'p'; const doc = new jsPDF(orientation, 'pt', 'a4'); console.log(`/* DEBUG ${uniqueId}: */ jsPDF object created inside .then(). Orientation: ${orientation}`); const page = doc.internal.pageSize; const margin = 40; const pageWidth = page.getWidth() - (margin * 2); const pageHeight = page.getHeight() - (margin * 2); const scale = Math.min(pageWidth / imgProps.width, pageHeight / imgProps.height); const scaledWidth = imgProps.width * scale; const scaledHeight = imgProps.height * scale; const marginLeft = (page.getWidth() - scaledWidth) / 2; const marginTop = (page.getHeight() - scaledHeight) / 2; console.log(`/* DEBUG ${uniqueId}: */ Adding image from html2canvas to PDF.`); doc.addImage(imgData, 'PNG', marginLeft, marginTop, scaledWidth, scaledHeight); console.log(`/* DEBUG ${uniqueId}: */ Attempting doc.save()...`); try { doc.save(pdfFilename); console.log(`/* DEBUG ${uniqueId}: */ doc.save() issued.`); } catch (saveError) { console.error(`/* DEBUG ${uniqueId}: */ Error during doc.save() [html2canvas]:`, saveError); errorMsgDiv.textContent = "Error saving the PDF. See console."; } if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } console.log(`/* DEBUG ${uniqueId}: */ html2canvas PDF process finished.`); }) .catch(err => { console.error(`/* DEBUG ${uniqueId}: */ Error during html2canvas capture:`, err); errorMsgDiv.textContent = "Error capturing map image for PDF. See console."; if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } }); } catch (error) { console.error(`/* DEBUG ${uniqueId}: */ Error setting up html2canvas generation:`, error); errorMsgDiv.textContent = "Setup error for PDF generation (html2canvas). See console."; if(downloadBtn) { downloadBtn.disabled = false; downloadBtn.textContent = 'Download Map as PDF'; downloadBtn.style.opacity = '1'; } } }, 10); // setTimeout } // End of downloadPDF // --- Initialize --- populateFeatures(); // Create checkboxes on load if(generateBtn) generateBtn.addEventListener('click', generateMap); else console.error("Generate Button not found after init checks!"); // Should not happen if init check passed if(downloadBtn) downloadBtn.addEventListener('click', downloadPDF); else console.error("Download Button not found after init checks!"); console.log("Map Maker Tool Initialized."); } // End of initializeMapMakerTool // --- Run Initialization --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeMapMakerTool); } else { initializeMapMakerTool(); // DOM already loaded }
Scroll to Top