Files
NeonPlaformer/editor.html

552 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>