onlinecodesimulator/editor.js

279 lines
8.1 KiB
JavaScript

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 = '<option>Error loading levels</option>';
}
}
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();