// Map Editor JavaScript let currentLocations = []; let availableNPCs = []; let selectedLocationId = null; let canvas, ctx; let scale = 50; let offsetX = 0; let offsetY = 0; // Check authentication on load window.addEventListener('DOMContentLoaded', async () => { const response = await fetch('/api/check-auth'); const data = await response.json(); if (data.authenticated) { showEditor(); } else { document.getElementById('loginContainer').style.display = 'flex'; } }); // Login form handler document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const password = document.getElementById('password').value; try { const response = await fetch('/api/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({password}) }); const data = await response.json(); if (data.success) { showEditor(); } else { showError(data.message); } } catch (error) { showError('Login failed: ' + error.message); } }); function showError(message) { const errorDiv = document.getElementById('loginError'); errorDiv.textContent = message; errorDiv.style.display = 'block'; } function showSuccess(message) { const successDiv = document.getElementById('saveSuccess'); successDiv.textContent = message; successDiv.style.display = 'block'; setTimeout(() => { successDiv.style.display = 'none'; }, 3000); } async function showEditor() { document.getElementById('loginContainer').style.display = 'none'; document.getElementById('editorContainer').style.display = 'grid'; // Initialize canvas canvas = document.getElementById('editorCanvas'); ctx = canvas.getContext('2d'); resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Load data await loadLocations(); await loadAvailableNPCs(); // Draw map drawMap(); // Canvas click handler canvas.addEventListener('click', handleCanvasClick); } function resizeCanvas() { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; drawMap(); } async function loadLocations() { try { const response = await fetch('/api/editor/locations'); const data = await response.json(); currentLocations = data.locations; renderLocationList(); } catch (error) { console.error('Failed to load locations:', error); } } async function loadAvailableNPCs() { try { const response = await fetch('/api/editor/available-npcs'); const data = await response.json(); availableNPCs = data.npcs; } catch (error) { console.error('Failed to load NPCs:', error); } } function renderLocationList() { const list = document.getElementById('locationList'); list.innerHTML = ''; currentLocations.forEach(location => { const item = document.createElement('div'); item.className = 'location-item'; if (location.id === selectedLocationId) { item.classList.add('active'); } item.innerHTML = `
${location.name}
📍 (${location.x}, ${location.y}) | Danger: ${location.danger_level}
`; item.onclick = () => selectLocation(location.id); list.appendChild(item); }); } async function selectLocation(locationId) { selectedLocationId = locationId; renderLocationList(); // Load full location details try { const response = await fetch(`/api/editor/location/${locationId}`); const location = await response.json(); populateForm(location); } catch (error) { console.error('Failed to load location details:', error); } drawMap(); } function populateForm(location) { document.getElementById('noSelectionMessage').classList.add('hidden'); document.getElementById('propertiesForm').classList.remove('hidden'); document.getElementById('locationId').value = location.id; document.getElementById('locationName').value = location.name; document.getElementById('locationDescription').value = location.description; document.getElementById('locationX').value = location.x; document.getElementById('locationY').value = location.y; document.getElementById('dangerLevel').value = location.danger_level; document.getElementById('encounterRate').value = location.encounter_rate; document.getElementById('wanderingChance').value = location.wandering_chance; document.getElementById('imagePath').value = location.image_path || ''; // Update image preview updateImagePreview(location.image_path); // Render spawn list renderSpawnList(location.spawn_npcs); } function updateImagePreview(imagePath) { const preview = document.getElementById('imagePreview'); if (imagePath) { preview.innerHTML = `Location image`; } else { preview.innerHTML = 'No image'; } } function renderSpawnList(spawns) { const list = document.getElementById('spawnList'); list.innerHTML = ''; spawns.forEach((spawn, index) => { const item = document.createElement('div'); item.className = 'spawn-item'; item.innerHTML = `
${spawn.emoji} ${spawn.name}
Weight: ${spawn.weight}
`; list.appendChild(item); }); } function drawMap() { ctx.fillStyle = '#0f0f1e'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw grid ctx.strokeStyle = '#1a1a3e'; ctx.lineWidth = 1; const centerX = canvas.width / 2; const centerY = canvas.height / 2; // Draw vertical lines for (let x = centerX % scale; x < canvas.width; x += scale) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } // Draw horizontal lines for (let y = centerY % scale; y < canvas.height; y += scale) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } // Draw axes ctx.strokeStyle = '#3a3a6a'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(centerX, 0); ctx.lineTo(centerX, canvas.height); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, centerY); ctx.lineTo(canvas.width, centerY); ctx.stroke(); // Draw locations currentLocations.forEach(location => { const screenX = centerX + location.x * scale; const screenY = centerY - location.y * scale; // Danger level colors const dangerColors = ['#4caf50', '#8bc34a', '#ffa726', '#ff5722', '#d32f2f']; const color = dangerColors[location.danger_level] || '#9e9e9e'; // Draw location circle ctx.fillStyle = color; ctx.beginPath(); ctx.arc(screenX, screenY, 15, 0, Math.PI * 2); ctx.fill(); // Highlight selected if (location.id === selectedLocationId) { ctx.strokeStyle = '#ffa726'; ctx.lineWidth = 3; ctx.stroke(); } // Draw label ctx.fillStyle = '#e0e0e0'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(location.name, screenX, screenY + 30); }); } function handleCanvasClick(event) { const rect = canvas.getBoundingClientRect(); const clickX = event.clientX - rect.left; const clickY = event.clientY - rect.top; const centerX = canvas.width / 2; const centerY = canvas.height / 2; // Check if clicked on existing location for (const location of currentLocations) { const screenX = centerX + location.x * scale; const screenY = centerY - location.y * scale; const distance = Math.sqrt(Math.pow(clickX - screenX, 2) + Math.pow(clickY - screenY, 2)); if (distance < 15) { selectLocation(location.id); return; } } // Clicked on empty space - create new location const worldX = ((clickX - centerX) / scale).toFixed(1); const worldY = (-(clickY - centerY) / scale).toFixed(1); if (confirm(`Create new location at (${worldX}, ${worldY})?`)) { createLocationAt(parseFloat(worldX), parseFloat(worldY)); } } function createLocationAt(x, y) { const newId = 'location_' + Date.now(); const newLocation = { id: newId, name: 'New Location', description: 'Enter description...', image_path: '', x: x, y: y, danger_level: 0, encounter_rate: 0.0, wandering_chance: 0.0, spawn_npcs: [] }; currentLocations.push(newLocation); selectedLocationId = newId; renderLocationList(); populateForm(newLocation); drawMap(); } function createNewLocation() { createLocationAt(0, 0); } async function saveLocation() { const locationData = { id: document.getElementById('locationId').value, name: document.getElementById('locationName').value, description: document.getElementById('locationDescription').value, x: parseFloat(document.getElementById('locationX').value), y: parseFloat(document.getElementById('locationY').value), danger_level: parseInt(document.getElementById('dangerLevel').value), encounter_rate: parseFloat(document.getElementById('encounterRate').value), wandering_chance: parseFloat(document.getElementById('wanderingChance').value), image_path: document.getElementById('imagePath').value, spawn_npcs: getCurrentSpawns() }; try { const response = await fetch('/api/editor/location', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(locationData) }); const data = await response.json(); if (data.success) { showSuccess('Location saved successfully!'); await loadLocations(); drawMap(); } else { alert('Failed to save: ' + data.error); } } catch (error) { alert('Failed to save location: ' + error.message); } } function getCurrentSpawns() { const spawns = []; const spawnItems = document.querySelectorAll('.spawn-item'); spawnItems.forEach(item => { const nameDiv = item.querySelector('.spawn-item-name'); const weightDiv = item.querySelector('.spawn-item-weight'); if (nameDiv && weightDiv) { const text = nameDiv.textContent; const npcName = text.substring(text.indexOf(' ') + 1).trim(); const weight = parseInt(weightDiv.textContent.replace('Weight: ', '')); // Find NPC ID by name const npc = availableNPCs.find(n => n.name === npcName); if (npc) { spawns.push({npc_id: npc.id, weight: weight}); } } }); return spawns; } function showAddSpawnModal() { const modal = document.getElementById('addSpawnModal'); const list = document.getElementById('npcSelectList'); list.innerHTML = ''; availableNPCs.forEach(npc => { const item = document.createElement('div'); item.className = 'npc-select-item'; item.innerHTML = `
${npc.emoji} ${npc.name}
HP: ${npc.hp_range[0]}-${npc.hp_range[1]} | DMG: ${npc.damage_range[0]}-${npc.damage_range[1]} | XP: ${npc.xp_reward}
`; item.onclick = () => addSpawn(npc); list.appendChild(item); }); modal.style.display = 'flex'; } function closeAddSpawnModal() { document.getElementById('addSpawnModal').style.display = 'none'; } function addSpawn(npc) { const weight = prompt(`Enter spawn weight for ${npc.name}:`, '50'); if (weight && !isNaN(weight)) { const list = document.getElementById('spawnList'); const item = document.createElement('div'); item.className = 'spawn-item'; item.innerHTML = `
${npc.emoji} ${npc.name}
Weight: ${weight}
`; list.appendChild(item); } closeAddSpawnModal(); } function removeSpawn(index) { const spawnItems = document.querySelectorAll('.spawn-item'); if (spawnItems[index]) { spawnItems[index].remove(); } } async function uploadImage() { const fileInput = document.getElementById('imageUpload'); const file = fileInput.files[0]; if (!file) return; const formData = new FormData(); formData.append('image', file); try { const response = await fetch('/api/editor/upload-image', { method: 'POST', body: formData }); const data = await response.json(); if (data.success) { document.getElementById('imagePath').value = data.image_path; updateImagePreview(data.image_path); showSuccess(data.message); } else { alert('Upload failed: ' + data.error); } } catch (error) { alert('Upload failed: ' + error.message); } } async function logout() { try { await fetch('/api/logout', {method: 'POST'}); window.location.reload(); } catch (error) { console.error('Logout failed:', error); } }