279 lines
8.1 KiB
JavaScript
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(); |