const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const gridSize = 10; let currentPolygon = []; let shapes = { robots: { player: { position: { x: 200, y: 200 } } }, waypoints: [], obstacles: [] }; let view = { zoom: 1, offsetX: 0, offsetY: 0 }; let loadedLevels = []; // store loaded levels here // Coordinate conversions function worldToScreen(x, y) { return { x: (x - view.offsetX) * view.zoom, y: (y - view.offsetY) * view.zoom }; } function screenToWorld(x, y) { return { x: x / view.zoom + view.offsetX, y: y / view.zoom + view.offsetY }; } // Draw grid function drawGrid() { const step = gridSize; const bounds = { left: view.offsetX, right: view.offsetX + canvas.width / view.zoom, top: view.offsetY, bottom: view.offsetY + canvas.height / view.zoom }; ctx.strokeStyle = "#eee"; ctx.lineWidth = 1; ctx.beginPath(); for (let x = Math.floor(bounds.left / step) * step; x < bounds.right; x += step) { const sx = worldToScreen(x, 0).x; ctx.moveTo(sx, 0); ctx.lineTo(sx, canvas.height); } for (let y = Math.floor(bounds.top / step) * step; y < bounds.bottom; y += step) { const sy = worldToScreen(0, y).y; ctx.moveTo(0, sy); ctx.lineTo(canvas.width, sy); } ctx.stroke(); } // Draw a circle (for vertices) function drawCircle(x, y, radius = 4) { const screen = worldToScreen(x, y); ctx.beginPath(); ctx.arc(screen.x, screen.y, radius, 0, Math.PI * 2); ctx.fill(); } // Draw polygon from absolute vertices function drawPolygon(vertices, stroke = "#000", fill = "#ccc") { if (vertices.length === 0) return; ctx.beginPath(); const first = worldToScreen(vertices[0].x, vertices[0].y); ctx.moveTo(first.x, first.y); for (let i = 1; i < vertices.length; i++) { const p = worldToScreen(vertices[i].x, vertices[i].y); ctx.lineTo(p.x, p.y); } ctx.closePath(); ctx.fillStyle = fill; ctx.fill(); ctx.strokeStyle = stroke; ctx.stroke(); } // Redraw entire canvas function redrawAll() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawGrid(); for (let w of shapes.waypoints) { drawPolygon(w.vertices, w.strokeColor, w.fillColor); } for (let o of shapes.obstacles) { drawPolygon(o.vertices, o.strokeColor, o.fillColor); } if (currentPolygon.length > 0) { ctx.strokeStyle = "#000"; ctx.fillStyle = "black"; for (let p of currentPolygon) { drawCircle(p.x, p.y); } ctx.beginPath(); const start = worldToScreen(currentPolygon[0].x, currentPolygon[0].y); ctx.moveTo(start.x, start.y); for (let i = 1; i < currentPolygon.length; i++) { const p = worldToScreen(currentPolygon[i].x, currentPolygon[i].y); ctx.lineTo(p.x, p.y); } ctx.stroke(); } } // Snap to grid if ctrl pressed function snapToGrid(x, y) { return { x: Math.round(x / gridSize) * gridSize, y: Math.round(y / gridSize) * gridSize }; } // Canvas click event to add vertices canvas.addEventListener("click", (e) => { const rect = canvas.getBoundingClientRect(); let sx = e.clientX - rect.left; let sy = e.clientY - rect.top; let { x, y } = screenToWorld(sx, sy); if (e.ctrlKey) { ({ x, y } = snapToGrid(x, y)); } if (currentPolygon.length > 0) { const dx = x - currentPolygon[0].x; const dy = y - currentPolygon[0].y; if (Math.sqrt(dx * dx + dy * dy) < 10 / view.zoom) { finishPolygon(); return; } } currentPolygon.push({ x, y }); redrawAll(); }); // Finish polygon and add to shapes function finishPolygon() { if (currentPolygon.length < 3) return; const type = document.getElementById("drawMode").value; const shape = { vertices: [...currentPolygon], strokeColor: type === "waypoints" ? "#0000FF" : "#999999", fillColor: type === "waypoints" ? "#0000CC" : "#CCCCCC" }; shapes[type].push(shape); currentPolygon = []; redrawAll(); } //document.getElementById("finishPolygon").addEventListener("click", finishPolygon); // Save JSON to file document.getElementById("saveJSON").addEventListener("click", () => { const levelName = document.getElementById("levelName").value; const exportData = [{ name: levelName, ...shapes }]; const blob = new Blob([JSON.stringify(exportData, null, 4)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = levelName.replace(/\s+/g, "_") + ".json"; a.click(); }); // Zoom with mouse wheel, zoom toward cursor canvas.addEventListener("wheel", (e) => { e.preventDefault(); const delta = -e.deltaY; const zoomFactor = 1.1; const mouse = screenToWorld(e.offsetX, e.offsetY); if (delta > 0) { view.zoom *= zoomFactor; } else { view.zoom /= zoomFactor; } view.offsetX = mouse.x - (e.offsetX / view.zoom); view.offsetY = mouse.y - (e.offsetY / view.zoom); redrawAll(); }, { passive: false }); // Undo with Ctrl+Z document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.key === "z") { if (currentPolygon.length > 0) { currentPolygon.pop(); } else { const type = document.getElementById("drawMode").value; if (shapes[type].length > 0) { shapes[type].pop(); } } redrawAll(); } }); // --------- NEW: Load Levels from data/levels.json --------- async function loadLevels() { try { const response = await fetch('data/levels.json'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); loadedLevels = await response.json(); const levelSelect = document.getElementById('levelSelect'); levelSelect.innerHTML = ''; // Clear loading message loadedLevels.forEach((level, idx) => { const option = document.createElement('option'); option.value = idx; option.textContent = level.name || `Level ${idx + 1}`; levelSelect.appendChild(option); }); } catch (err) { alert('Failed to load levels.json: ' + err.message); const levelSelect = document.getElementById('levelSelect'); levelSelect.innerHTML = ''; } } function loadSelectedLevel() { const levelSelect = document.getElementById('levelSelect'); const idx = parseInt(levelSelect.value); if (isNaN(idx) || !loadedLevels[idx]) { alert('Please select a valid level'); return; } const level = loadedLevels[idx]; document.getElementById('levelName').value = level.name || ''; if (level.robots && level.robots.player && level.robots.player.position) { shapes.robots.player.position = { ...level.robots.player.position }; } else { shapes.robots.player.position = { x: 200, y: 200 }; } shapes.waypoints = (level.waypoints || []).map(wp => ({ vertices: wp.vertices.map(v => ({ x: v.x, y: v.y })), strokeColor: wp.strokeColor || "#0000FF", fillColor: wp.fillColor || "#0000CC" })); shapes.obstacles = (level.obstacles || []).map(ob => ({ vertices: ob.vertices.map(v => ({ x: v.x, y: v.y })), strokeColor: ob.strokeColor || "#999999", fillColor: ob.fillColor || "#CCCCCC" })); currentPolygon = []; redrawAll(); } document.getElementById('loadLevelBtn').addEventListener('click', loadSelectedLevel); // Load levels.json on page load loadLevels(); // Initial draw redrawAll();