Загрузить файлы в «/»

This commit is contained in:
2026-02-24 18:21:35 +03:00
parent 055b6468d4
commit 27834dab06
2 changed files with 693 additions and 0 deletions

551
editor.html Normal file
View File

@@ -0,0 +1,551 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎮 Редактор Уровней</title>
<link rel="stylesheet" href="css/style.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
background: linear-gradient(135deg, #050510 0%, #0a0a20 50%, #0f0a25 100%);
min-height: 100vh;
font-family: 'Rajdhani', sans-serif;
}
h1 {
color: #00ffff;
font-family: 'Orbitron', sans-serif;
text-shadow: 0 0 15px rgba(0, 255, 255, 0.5);
margin: 8px 0;
font-size: 22px;
}
.top-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
select, input {
padding: 8px 12px;
background: rgba(0, 40, 60, 0.9);
border: 1px solid rgba(0, 255, 255, 0.4);
border-radius: 6px;
color: #fff;
font-size: 14px;
}
select:focus, input:focus {
outline: none;
border-color: #00ffff;
box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
}
#levelName { width: 140px; }
.canvas-box {
border: 2px solid #00ffff;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.15);
overflow: hidden;
}
#editorCanvas {
display: block;
cursor: crosshair;
background: #080812;
}
.toolbar-row {
display: flex;
gap: 6px;
margin-top: 10px;
flex-wrap: wrap;
justify-content: center;
max-width: 920px;
}
.tool-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 12px;
background: rgba(0, 255, 255, 0.08);
border: 1px solid rgba(0, 255, 255, 0.2);
border-radius: 6px;
color: #aaa;
cursor: pointer;
font-size: 13px;
transition: all 0.15s;
}
.tool-btn:hover {
background: rgba(0, 255, 255, 0.15);
color: #fff;
}
.tool-btn.active {
background: rgba(0, 255, 255, 0.3);
border-color: #00ffff;
color: #fff;
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
}
.action-btn {
padding: 8px 14px;
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 6px;
color: #fff;
cursor: pointer;
font-weight: 600;
font-size: 13px;
}
.action-btn.save { background: rgba(50, 200, 80, 0.3); }
.action-btn.test { background: rgba(255, 80, 100, 0.3); }
.action-btn.clear { background: rgba(100, 100, 100, 0.3); }
.action-btn:hover { box-shadow: 0 0 12px rgba(0, 255, 255, 0.3); }
.info-bar {
margin-top: 8px;
padding: 8px 15px;
background: rgba(0, 0, 0, 0.4);
border-radius: 6px;
font-size: 12px;
color: #777;
}
.info-bar span { color: #00ffff; font-weight: bold; }
.hint {
color: #666;
font-size: 11px;
margin-top: 5px;
}
</style>
</head>
<body>
<h1>🎮 Редактор Уровней</h1>
<div class="top-row">
<select id="levelSelect" onchange="loadLevelFromList()">
<option value="">-- Выберите уровень --</option>
</select>
<input type="text" id="levelName" placeholder="Название">
<button class="action-btn save" onclick="saveToFile()">💾 Сохранить</button>
<button class="action-btn test" onclick="testLevel()">▶ Тест</button>
<button class="action-btn clear" onclick="clearLevel()">🗑️ Очистить</button>
</div>
<div class="canvas-box">
<canvas id="editorCanvas" width="900" height="480"></canvas>
</div>
<p class="hint">ЛКМ: разместить | ПКМ: удалить | Перетаскивание: двигать объекты</p>
<div class="toolbar-row">
<button class="tool-btn active" data-tool="platform"><span>🟫</span> Платформа</button>
<button class="tool-btn" data-tool="ground"><span></span> Земля</button>
<button class="tool-btn" data-tool="moving"><span>↔️</span> Движущаяся</button>
<button class="tool-btn" data-tool="coin"><span>💰</span> Монета</button>
<button class="tool-btn" data-tool="spike"><span>🔺</span> Шип</button>
<button class="tool-btn" data-tool="enemy"><span>👾</span> Враг</button>
<button class="tool-btn" data-tool="powerup"><span></span> Бонус</button>
<button class="tool-btn" data-tool="flag"><span>🚩</span> Флаг</button>
<button class="tool-btn" data-tool="checkpoint"><span>💠</span> Чекпоинт</button>
<button class="tool-btn" data-tool="player"><span>👤</span> Игрок</button>
<button class="tool-btn" data-tool="eraser"><span></span> Ластик</button>
</div>
<div class="info-bar">
Объектов: <span id="objCount">0</span> | ЛКМ: разместить | ПКМ: удалить | Drag: переместить
</div>
<script src="js/levels.js"></script>
<script>
const canvas = document.getElementById('editorCanvas');
const ctx = canvas.getContext('2d');
let currentTool = 'platform';
let objects = [];
let playerStart = { x: 60, y: 400 };
let flagPos = { x: 800, y: 420 };
// Drag state
let dragging = null;
let dragOffset = { x: 0, y: 0 };
// Populate level selector
const select = document.getElementById('levelSelect');
LEVELS.forEach((level, i) => {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = (i + 1) + '. ' + level.name;
select.appendChild(opt);
});
// Tool buttons
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTool = btn.dataset.tool;
});
});
function loadLevelFromList() {
const idx = parseInt(document.getElementById('levelSelect').value);
if (isNaN(idx)) return;
const level = LEVELS[idx];
objects = [];
// Load platforms - check isGround flag or color
level.platforms?.forEach(p => {
const isGround = p.isGround || p.width >= 800 || p.x < 40;
const type = isGround ? 'ground' : 'platform';
objects.push({ type: type, x: p.x, y: p.y, width: p.width, height: p.height, color: p.color });
});
level.movingPlatforms?.forEach(p => objects.push({ type: 'moving', x: p.x, y: p.y, width: p.width, height: p.height, color: p.color, startX: p.startX, endX: p.endX, speed: p.speed }));
level.coins?.forEach(c => objects.push({ type: 'coin', x: c.x, y: c.y, width: 20, height: 20 }));
level.spikes?.forEach(s => objects.push({ type: 'spike', x: s.x, y: s.y, width: 30, height: 20 }));
level.patrolEnemies?.forEach(e => objects.push({ type: 'enemy', x: e.x, y: e.y, width: 30, height: 30, patrolLeft: e.patrolLeft, patrolRight: e.patrolRight }));
level.powerUps?.forEach(p => objects.push({ type: 'powerup', x: p.x, y: p.y, width: 24, height: 24, powerType: p.type }));
level.checkpoints?.forEach(c => objects.push({ type: 'checkpoint', x: c.x, y: c.y, width: 30, height: 40 }));
playerStart = level.playerStart || { x: 60, y: 400 };
flagPos = level.flag || { x: 800, y: 420 };
document.getElementById('levelName').value = level.name;
document.getElementById('objCount').textContent = objects.length;
render();
}
function getPos(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
};
}
function findObjectAt(pos) {
// Check player
if (pos.x >= playerStart.x && pos.x <= playerStart.x + 28 && pos.y >= playerStart.y && pos.y <= playerStart.y + 38) {
return { type: 'player' };
}
// Check flag
if (pos.x >= flagPos.x && pos.x <= flagPos.x + 30 && pos.y >= flagPos.y && pos.y <= flagPos.y + 50) {
return { type: 'flag' };
}
// Check objects (reverse for top-most)
for (let i = objects.length - 1; i >= 0; i--) {
const o = objects[i];
const threshold = ['coin','spike','enemy','powerup','checkpoint'].includes(o.type) ? 25 : 40;
if (Math.abs(o.x + (o.width || 20)/2 - pos.x) < threshold && Math.abs(o.y + (o.height||20)/2 - pos.y) < threshold) {
return { type: 'object', index: i };
}
}
return null;
}
canvas.addEventListener('mousedown', e => {
const pos = getPos(e);
const found = findObjectAt(pos);
if (found) {
// Start dragging
if (found.type === 'player') {
dragging = { type: 'player', offsetX: pos.x - playerStart.x, offsetY: pos.y - playerStart.y };
} else if (found.type === 'flag') {
dragging = { type: 'flag', offsetX: pos.x - flagPos.x, offsetY: pos.y - flagPos.y };
} else if (found.type === 'object') {
const o = objects[found.index];
dragging = { type: 'object', index: found.index, offsetX: pos.x - o.x, offsetY: pos.y - o.y };
}
return;
}
// Place new object
if (e.button === 0) { // Left click
placeObject(pos);
} else if (e.button === 2) { // Right click
removeObject(pos);
}
});
canvas.addEventListener('mousemove', e => {
const pos = getPos(e);
if (dragging) {
const snap = 10;
let newX, newY;
if (dragging.type === 'player') {
newX = Math.round((pos.x - dragging.offsetX) / snap) * snap;
newY = Math.round((pos.y - dragging.offsetY) / snap) * snap;
playerStart = { x: Math.max(0, Math.min(850, newX)), y: Math.max(0, Math.min(440, newY)) };
} else if (dragging.type === 'flag') {
newX = Math.round((pos.x - dragging.offsetX) / snap) * snap;
newY = Math.round((pos.y - dragging.offsetY) / snap) * snap;
flagPos = { x: Math.max(0, Math.min(850, newX)), y: Math.max(0, Math.min(430, newY)) };
} else if (dragging.type === 'object') {
newX = Math.round((pos.x - dragging.offsetX) / snap) * snap;
newY = Math.round((pos.y - dragging.offsetY) / snap) * snap;
const o = objects[dragging.index];
o.x = Math.max(0, Math.min(870 - o.width, newX));
o.y = Math.max(0, Math.min(460 - o.height, newY));
}
render();
} else {
// Cursor highlight
render();
const found = findObjectAt(pos);
if (found) {
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
if (found.type === 'player') {
ctx.strokeRect(playerStart.x - 2, playerStart.y - 2, 32, 42);
} else if (found.type === 'flag') {
ctx.strokeRect(flagPos.x - 2, flagPos.y - 2, 34, 54);
} else {
const o = objects[found.index];
ctx.strokeRect(o.x - 2, o.y - 2, (o.width || 20) + 4, (o.height || 20) + 4);
}
}
}
});
canvas.addEventListener('mouseup', () => dragging = null);
canvas.addEventListener('mouseleave', () => dragging = null);
canvas.addEventListener('contextmenu', e => { e.preventDefault(); removeObject(getPos(e)); });
function placeObject(pos) {
const snap = 10;
const x = Math.round(pos.x / snap) * snap;
const y = Math.round(pos.y / snap) * snap;
if (currentTool === 'eraser') return;
// Check if already exists
const exists = findObjectAt(pos);
if (exists) return;
switch(currentTool) {
case 'player': playerStart = { x: x - 14, y: y - 19 }; break;
case 'flag': flagPos = { x: x - 2, y: y - 25 }; break;
case 'platform': objects.push({ type: 'platform', x: x - 50, y: y - 10, width: 100, height: 20, color: '#4a3a6a' }); break;
case 'ground': objects.push({ type: 'ground', x: x - 50, y: y - 20, width: 100, height: 40, color: '#2a2a4a' }); break;
case 'moving': objects.push({ type: 'moving', x: x - 40, y: y - 10, width: 80, height: 20, color: '#6a5a8a', startX: x - 80, endX: x + 80, speed: 2 }); break;
case 'coin': objects.push({ type: 'coin', x: x - 10, y: y - 10, width: 20, height: 20 }); break;
case 'spike': objects.push({ type: 'spike', x: x - 15, y: y - 10, width: 30, height: 20 }); break;
case 'enemy': objects.push({ type: 'enemy', x: x - 15, y: y - 15, width: 30, height: 30, patrolLeft: x - 80, patrolRight: x + 80 }); break;
case 'powerup': objects.push({ type: 'powerup', x: x - 12, y: y - 12, width: 24, height: 24, powerType: 'doubleJump' }); break;
case 'checkpoint': objects.push({ type: 'checkpoint', x: x - 15, y: y - 20, width: 30, height: 40 }); break;
}
document.getElementById('objCount').textContent = objects.length;
render();
}
function removeObject(pos) {
const found = findObjectAt(pos);
if (found && found.type === 'object') {
objects.splice(found.index, 1);
document.getElementById('objCount').textContent = objects.length;
render();
}
}
function render() {
const w = canvas.width, h = canvas.height;
// Background
ctx.fillStyle = '#080812';
ctx.fillRect(0, 0, w, h);
// Grid
ctx.strokeStyle = 'rgba(0, 255, 255, 0.05)';
for (let x = 0; x <= w; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); }
for (let y = 0; y <= h; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
// Objects
objects.forEach(obj => {
switch(obj.type) {
case 'platform': case 'ground': case 'moving':
ctx.fillStyle = obj.color;
ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.strokeRect(obj.x, obj.y, obj.width, obj.height);
break;
case 'coin':
ctx.fillStyle = '#ffd700';
ctx.beginPath(); ctx.arc(obj.x + 10, obj.y + 10, 10, 0, Math.PI * 2); ctx.fill();
break;
case 'spike':
ctx.fillStyle = '#ff4444';
ctx.beginPath(); ctx.moveTo(obj.x + 15, obj.y); ctx.lineTo(obj.x + 30, obj.y + 20); ctx.lineTo(obj.x, obj.y + 20); ctx.closePath(); ctx.fill();
break;
case 'enemy':
ctx.fillStyle = '#ff00ff';
ctx.beginPath(); ctx.arc(obj.x + 15, obj.y + 15, 14, 0, Math.PI * 2); ctx.fill();
break;
case 'powerup':
ctx.fillStyle = '#00ff88';
ctx.beginPath(); ctx.arc(obj.x + 12, obj.y + 12, 12, 0, Math.PI * 2); ctx.fill();
break;
case 'checkpoint':
ctx.fillStyle = '#00aa00';
ctx.fillRect(obj.x, obj.y, 4, 40);
ctx.fillRect(obj.x, obj.y, 25, 15);
break;
}
});
// Player
ctx.fillStyle = '#00d9ff';
ctx.fillRect(playerStart.x, playerStart.y, 28, 38);
ctx.strokeStyle = '#00ffff';
ctx.lineWidth = 2;
ctx.strokeRect(playerStart.x, playerStart.y, 28, 38);
// Flag
ctx.fillStyle = '#44ff44';
ctx.fillRect(flagPos.x, flagPos.y, 4, 50);
ctx.beginPath();
ctx.moveTo(flagPos.x + 4, flagPos.y);
ctx.lineTo(flagPos.x + 30, flagPos.y + 15);
ctx.lineTo(flagPos.x + 4, flagPos.y + 30);
ctx.closePath(); ctx.fill();
}
function clearLevel() {
if (confirm('Очистить уровень?')) {
objects = [];
playerStart = { x: 60, y: 400 };
flagPos = { x: 800, y: 420 };
document.getElementById('objCount').textContent = '0';
render();
}
}
function saveToFile() {
const name = document.getElementById('levelName').value || 'Новый уровень';
// Build the level object
const platforms = objects.filter(o => o.type === 'platform' || o.type === 'ground');
const movingPlatforms = objects.filter(o => o.type === 'moving');
const coins = objects.filter(o => o.type === 'coin');
const spikes = objects.filter(o => o.type === 'spike');
const enemies = objects.filter(o => o.type === 'enemy');
const powerups = objects.filter(o => o.type === 'powerup');
const checkpoints = objects.filter(o => o.type === 'checkpoint');
// Generate the code in the same format as levels.js
let code = ` // ${name}\n {\n`;
code += ` name: "${name}",\n`;
code += ` playerStart: { x: ${playerStart.x}, y: ${playerStart.y} },\n`;
code += ` platforms: [\n`;
platforms.forEach((p, i) => {
const isGround = p.type === 'ground' || p.width >= 800 || p.x < 35;
const comment = isGround ? '// ' + (p.width >= 800 ? 'Ground' : 'Wall') : '// Platform';
code += ` ${comment}\n`;
code += ` { x: ${p.x}, y: ${p.y}, width: ${p.width}, height: ${p.height}, color: '${p.color}', ${isGround ? 'isGround: true' : ''} },\n`;
});
code += ` ],\n`;
if (movingPlatforms.length > 0) {
code += ` movingPlatforms: [\n`;
movingPlatforms.forEach(p => {
code += ` { x: ${p.x}, y: ${p.y}, width: ${p.width}, height: ${p.height}, color: '${p.color}', startX: ${p.startX}, endX: ${p.endX}, speed: ${p.speed}, direction: 1, moveType: 'horizontal' },\n`;
});
code += ` ],\n`;
} else {
code += ` movingPlatforms: [],\n`;
}
code += ` coins: [\n`;
coins.forEach(c => {
code += ` { x: ${c.x}, y: ${c.y} },\n`;
});
code += ` ],\n`;
code += ` spikes: [\n`;
spikes.forEach(s => {
code += ` { x: ${s.x}, y: ${s.y}, width: ${s.width}, height: ${s.height} },\n`;
});
code += ` ],\n`;
code += ` patrolEnemies: [\n`;
enemies.forEach(e => {
code += ` { x: ${e.x}, y: ${e.y}, patrolLeft: ${e.patrolLeft}, patrolRight: ${e.patrolRight} },\n`;
});
code += ` ],\n`;
code += ` powerUps: [\n`;
powerups.forEach(p => {
code += ` { x: ${p.x}, y: ${p.y}, type: '${p.powerType}' },\n`;
});
code += ` ],\n`;
code += ` checkpoints: [\n`;
checkpoints.forEach(c => {
code += ` { x: ${c.x}, y: ${c.y} },\n`;
});
code += ` ],\n`;
code += ` flag: { x: ${flagPos.x}, y: ${flagPos.y} },\n`;
code += ` portal: null\n }`;
// Copy to clipboard
navigator.clipboard.writeText(code).then(() => {
alert('Код скопирован в буфер обмена!\nТеперь можно вставить в levels.js');
}).catch(() => {
// Fallback - download as file
const blob = new Blob([code], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'level.js.txt';
a.click();
});
}
function testLevel() {
if (objects.length === 0) { alert('Добавьте объекты!'); return; }
const name = document.getElementById('levelName').value || 'Тест';
const levelData = {
name: name,
playerStart: playerStart,
platforms: objects.filter(o => o.type === 'platform').map(p => ({ ...p, isGround: false })),
movingPlatforms: objects.filter(o => o.type === 'moving').map(p => ({ ...p, direction: 1, moveType: 'horizontal' })),
coins: objects.filter(o => o.type === 'coin').map(c => ({ x: c.x, y: c.y })),
spikes: objects.filter(o => o.type === 'spike').map(s => ({ x: s.x, y: s.y })),
patrolEnemies: objects.filter(o => o.type === 'enemy').map(e => ({ x: e.x, y: e.y, patrolLeft: e.patrolLeft, patrolRight: e.patrolRight })),
powerUps: objects.filter(o => o.type === 'powerup').map(p => ({ x: p.x, y: p.y, type: p.powerType })),
checkpoints: objects.filter(o => o.type === 'checkpoint').map(c => ({ x: c.x, y: c.y })),
flag: flagPos,
portal: null
};
localStorage.setItem('testLevel', JSON.stringify(levelData));
window.open('index.html?test=1', '_blank');
}
render();
</script>
</body>
</html>