Virtual Fantasy Kingdom Map Maker
Design Your Realm
Your Kingdom Map
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${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}` : ''}${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
}
