This commit is contained in:
2025-10-20 19:35:38 +05:00
commit 023ccd03d8
42 changed files with 10007 additions and 0 deletions

573
map-editor/editor.js Normal file
View File

@@ -0,0 +1,573 @@
// Конфигурация редактора
const CELL_SIZE = 30;
const GRID_COLOR = '#dee2e6';
const COLORS = {
0: '#f8f9fa', // Дорога
1: '#6c757d', // Камень
2: '#e3f2fd', // Снег
3: '#b3e5fc', // Лёд
4: '#fff3cd', // Чекпоинт
5: '#d4edda' // Старт
};
// Состояние редактора
let width = 15;
let height = 15;
let map = [];
let selectedType = 0;
let isDrawing = false;
// Состояние визуализации
let solution = null;
let trajectory = [];
let currentStep = 0;
let isPlaying = false;
let playbackSpeed = 5;
let playbackInterval = null;
let startPosition = null;
// Canvas элементы
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
// Инициализация
function init() {
width = parseInt(document.getElementById('width').value);
height = parseInt(document.getElementById('height').value);
initMap();
resizeCanvas();
drawMap();
}
// Инициализация карты
function initMap() {
map = Array(height).fill(null).map(() => Array(width).fill(0));
}
// Изменение размера canvas
function resizeCanvas() {
canvas.width = width * CELL_SIZE;
canvas.height = height * CELL_SIZE;
}
// Отрисовка карты
function drawMap() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Рисуем ячейки
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const cellType = map[y][x];
ctx.fillStyle = COLORS[cellType];
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
}
// Рисуем сетку
ctx.strokeStyle = GRID_COLOR;
ctx.lineWidth = 1;
// Вертикальные линии
for (let x = 0; x <= width; x++) {
ctx.beginPath();
ctx.moveTo(x * CELL_SIZE, 0);
ctx.lineTo(x * CELL_SIZE, height * CELL_SIZE);
ctx.stroke();
}
// Горизонтальные линии
for (let y = 0; y <= height; y++) {
ctx.beginPath();
ctx.moveTo(0, y * CELL_SIZE);
ctx.lineTo(width * CELL_SIZE, y * CELL_SIZE);
ctx.stroke();
}
// Рисуем маркеры для чекпоинтов и старта
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (map[y][x] === 4) {
ctx.fillStyle = '#856404';
ctx.fillText('C', x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2);
} else if (map[y][x] === 5) {
ctx.fillStyle = '#155724';
ctx.fillText('S', x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2);
}
}
}
// Рисуем визуализацию траектории, если она есть
if (trajectory.length > 0) {
drawTrajectory();
}
}
// Рисование траектории решения
function drawTrajectory() {
if (!trajectory || trajectory.length === 0) return;
// Рисуем все предыдущие позиции как след
ctx.strokeStyle = '#667eea';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
for (let i = 0; i <= currentStep && i < trajectory.length; i++) {
const pos = trajectory[i];
const screenX = pos.x * CELL_SIZE + CELL_SIZE / 2;
const screenY = pos.y * CELL_SIZE + CELL_SIZE / 2;
if (i === 0) {
ctx.moveTo(screenX, screenY);
} else {
ctx.lineTo(screenX, screenY);
}
}
ctx.stroke();
// Рисуем точки на каждом шаге
for (let i = 0; i <= currentStep && i < trajectory.length; i++) {
const pos = trajectory[i];
const screenX = pos.x * CELL_SIZE + CELL_SIZE / 2;
const screenY = pos.y * CELL_SIZE + CELL_SIZE / 2;
ctx.beginPath();
ctx.arc(screenX, screenY, 4, 0, Math.PI * 2);
ctx.fillStyle = '#667eea';
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
}
// Рисуем текущую позицию большим кругом
if (currentStep < trajectory.length) {
const current = trajectory[currentStep];
const screenX = current.x * CELL_SIZE + CELL_SIZE / 2;
const screenY = current.y * CELL_SIZE + CELL_SIZE / 2;
// Пульсирующий эффект
ctx.beginPath();
ctx.arc(screenX, screenY, 10, 0, Math.PI * 2);
ctx.fillStyle = '#f5576c';
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 3;
ctx.stroke();
// Стрелка направления скорости
if (current.vx !== 0 || current.vy !== 0) {
const arrowLen = 20;
const angle = Math.atan2(current.vy, current.vx);
const endX = screenX + Math.cos(angle) * arrowLen;
const endY = screenY + Math.sin(angle) * arrowLen;
ctx.beginPath();
ctx.moveTo(screenX, screenY);
ctx.lineTo(endX, endY);
ctx.strokeStyle = '#f5576c';
ctx.lineWidth = 3;
ctx.stroke();
// Наконечник стрелки
const headLen = 8;
const headAngle = Math.PI / 6;
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(
endX - headLen * Math.cos(angle - headAngle),
endY - headLen * Math.sin(angle - headAngle)
);
ctx.moveTo(endX, endY);
ctx.lineTo(
endX - headLen * Math.cos(angle + headAngle),
endY - headLen * Math.sin(angle + headAngle)
);
ctx.stroke();
}
}
}
// Выбор типа ячейки
function selectCellType(type) {
selectedType = type;
// Обновляем UI
document.querySelectorAll('.cell-type').forEach(el => {
el.classList.remove('active');
});
document.querySelector(`[data-type="${type}"]`).classList.add('active');
}
// Получение координат ячейки по клику
function getCellCoords(event) {
const rect = canvas.getBoundingClientRect();
const x = Math.floor((event.clientX - rect.left) / CELL_SIZE);
const y = Math.floor((event.clientY - rect.top) / CELL_SIZE);
if (x >= 0 && x < width && y >= 0 && y < height) {
return { x, y };
}
return null;
}
// Установка типа ячейки
function setCellType(x, y) {
if (x >= 0 && x < width && y >= 0 && y < height) {
map[y][x] = selectedType;
drawMap();
}
}
// Обработчики событий мыши
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
const coords = getCellCoords(e);
if (coords) {
setCellType(coords.x, coords.y);
}
});
canvas.addEventListener('mousemove', (e) => {
if (isDrawing) {
const coords = getCellCoords(e);
if (coords) {
setCellType(coords.x, coords.y);
}
}
});
canvas.addEventListener('mouseup', () => {
isDrawing = false;
});
canvas.addEventListener('mouseleave', () => {
isDrawing = false;
});
// Изменение размера карты
function resizeMap() {
const newWidth = parseInt(document.getElementById('width').value);
const newHeight = parseInt(document.getElementById('height').value);
if (newWidth < 5 || newWidth > 100 || newHeight < 5 || newHeight > 100) {
alert('Размеры должны быть от 5 до 100');
return;
}
const newMap = Array(newHeight).fill(null).map(() => Array(newWidth).fill(0));
// Копируем существующие данные
const minHeight = Math.min(height, newHeight);
const minWidth = Math.min(width, newWidth);
for (let y = 0; y < minHeight; y++) {
for (let x = 0; x < minWidth; x++) {
newMap[y][x] = map[y][x];
}
}
width = newWidth;
height = newHeight;
map = newMap;
resizeCanvas();
drawMap();
}
// Очистка карты
function clearMap() {
if (confirm('Вы уверены, что хотите очистить всю карту?')) {
initMap();
drawMap();
}
}
// Экспорт карты в JSON
function exportMap() {
const data = {
map: map
};
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `racing-map-${width}x${height}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Также выводим в консоль для быстрого копирования
console.log('Экспортированная карта:', json);
alert('Карта экспортирована! JSON также выведен в консоль браузера (F12)');
}
// Импорт карты из JSON
function importMap(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!data.map || !Array.isArray(data.map)) {
throw new Error('Неверный формат: отсутствует массив map');
}
const newMap = data.map;
// Валидация
if (!newMap.every(row => Array.isArray(row))) {
throw new Error('Неверный формат: map должен быть двумерным массивом');
}
const newHeight = newMap.length;
const newWidth = newMap[0].length;
if (newHeight < 5 || newHeight > 100 || newWidth < 5 || newWidth > 100) {
throw new Error('Размеры карты должны быть от 5 до 100');
}
if (!newMap.every(row => row.length === newWidth)) {
throw new Error('Все строки должны иметь одинаковую длину');
}
// Проверка значений ячеек
const validValues = [0, 1, 2, 3, 4, 5];
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
if (!validValues.includes(newMap[y][x])) {
throw new Error(`Недопустимое значение ячейки: ${newMap[y][x]} на позиции [${y}][${x}]`);
}
}
}
// Импортируем карту
height = newHeight;
width = newWidth;
map = newMap;
// Обновляем UI
document.getElementById('width').value = width;
document.getElementById('height').value = height;
resizeCanvas();
drawMap();
alert('Карта успешно импортирована!');
} catch (error) {
alert('Ошибка при импорте: ' + error.message);
console.error('Ошибка импорта:', error);
}
};
reader.readAsText(file);
// Сброс input для возможности повторного импорта того же файла
event.target.value = '';
}
// Обработчики изменения размеров
document.getElementById('width').addEventListener('keypress', (e) => {
if (e.key === 'Enter') resizeMap();
});
document.getElementById('height').addEventListener('keypress', (e) => {
if (e.key === 'Enter') resizeMap();
});
// ============================================
// Функции визуализации решения
// ============================================
// Поиск стартовой позиции на карте
function findStartPosition() {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (map[y][x] === 5) {
return { x, y };
}
}
}
return null;
}
// Симуляция траектории на основе векторов ускорений
function simulateTrajectory(accelerations, start) {
const traj = [];
let x = start.x;
let y = start.y;
let vx = 0;
let vy = 0;
// Начальная позиция
traj.push({ x, y, vx, vy, ax: 0, ay: 0 });
// Применяем каждое ускорение
for (let i = 0; i < accelerations.length; i++) {
const [ax, ay] = accelerations[i];
// Обновляем скорость
vx += ax;
vy += ay;
// Обновляем позицию
x += vx;
y += vy;
traj.push({ x, y, vx, vy, ax, ay });
}
return traj;
}
// Загрузка решения из JSON
function loadSolution(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!data.solution || !Array.isArray(data.solution)) {
throw new Error('Неверный формат: отсутствует массив solution');
}
// Проверяем, что это массив массивов из двух чисел
if (!data.solution.every(acc => Array.isArray(acc) && acc.length === 2)) {
throw new Error('Неверный формат: solution должен быть массивом [[ax, ay], ...]');
}
// Находим стартовую позицию
startPosition = findStartPosition();
if (!startPosition) {
throw new Error('На карте не найдена точка старта (тип 5)');
}
solution = data.solution;
trajectory = simulateTrajectory(solution, startPosition);
currentStep = 0;
// Показываем панель визуализации
document.getElementById('visualizationPanel').classList.remove('hidden');
document.getElementById('clearVisBtn').disabled = false;
// Обновляем информацию
updateStepInfo();
drawMap();
alert(`Решение загружено! ${solution.length} шагов.`);
} catch (error) {
alert('Ошибка при загрузке решения: ' + error.message);
console.error('Ошибка загрузки:', error);
}
};
reader.readAsText(file);
event.target.value = '';
}
// Обновление информации о текущем шаге
function updateStepInfo() {
if (!trajectory || trajectory.length === 0) return;
const current = trajectory[currentStep];
document.getElementById('stepNumber').textContent = `${currentStep} / ${trajectory.length - 1}`;
document.getElementById('positionValue').textContent = `(${current.x}, ${current.y})`;
document.getElementById('velocityValue').textContent = `(${current.vx}, ${current.vy})`;
document.getElementById('accelerationValue').textContent = `(${current.ax}, ${current.ay})`;
}
// Воспроизведение визуализации
function playVisualization() {
if (!trajectory || trajectory.length === 0) return;
isPlaying = true;
document.getElementById('playBtn').disabled = true;
document.getElementById('pauseBtn').disabled = false;
playbackInterval = setInterval(() => {
if (currentStep < trajectory.length - 1) {
currentStep++;
updateStepInfo();
drawMap();
} else {
pauseVisualization();
}
}, 1000 / playbackSpeed);
}
// Пауза воспроизведения
function pauseVisualization() {
isPlaying = false;
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
if (playbackInterval) {
clearInterval(playbackInterval);
playbackInterval = null;
}
}
// Сброс визуализации
function resetVisualization() {
pauseVisualization();
currentStep = 0;
updateStepInfo();
drawMap();
}
// Шаг вперед
function stepForward() {
if (!trajectory || trajectory.length === 0) return;
if (currentStep < trajectory.length - 1) {
currentStep++;
updateStepInfo();
drawMap();
}
}
// Обновление скорости воспроизведения
function updateSpeed() {
playbackSpeed = parseInt(document.getElementById('speedSlider').value);
document.getElementById('speedValue').textContent = `${playbackSpeed}x`;
// Если воспроизведение идет, перезапускаем с новой скоростью
if (isPlaying) {
pauseVisualization();
playVisualization();
}
}
// Очистка визуализации
function clearVisualization() {
pauseVisualization();
solution = null;
trajectory = [];
currentStep = 0;
startPosition = null;
document.getElementById('visualizationPanel').classList.add('hidden');
document.getElementById('clearVisBtn').disabled = true;
drawMap();
}
// Инициализация при загрузке страницы
init();