Загрузить файлы в «/»
This commit is contained in:
551
editor.html
Normal file
551
editor.html
Normal 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>
|
||||
142
index.html
Normal file
142
index.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!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">
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<h2>ЗАГРУЗКА...</h2>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Menu -->
|
||||
<div id="main-menu">
|
||||
<h1>🚀 ПЛАТФОРМЕР</h1>
|
||||
<div class="menu-buttons">
|
||||
<button class="menu-btn" onclick="startGame()">🎮 ИГРАТЬ</button>
|
||||
<button class="menu-btn" onclick="showSettings()">⚙️ НАСТРОЙКИ</button>
|
||||
<button class="menu-btn" onclick="showAbout()">ℹ️ ОБ ИГРЕ</button>
|
||||
</div>
|
||||
<p class="menu-hint">Нажмите любую клавишу или кнопку для старта</p>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>⚙️ НАСТРОЙКИ</h2>
|
||||
<div class="setting-item">
|
||||
<span>🔊 Звук</span>
|
||||
<button id="sound-toggle" onclick="toggleSound()">ВКЛ</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<span>🖥️ Полный экран</span>
|
||||
<button onclick="toggleFullscreen()">ВКЛ</button>
|
||||
</div>
|
||||
<button class="menu-btn back-btn" onclick="hideSettings()">← НАЗАД</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Modal -->
|
||||
<div id="about-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>ℹ️ ОБ ИГРЕ</h2>
|
||||
<p>Платформер v2.0</p>
|
||||
<p>Создано с ❤️</p>
|
||||
<p>Управление: ← → движение, Пробел - прыжок</p>
|
||||
<button class="menu-btn back-btn" onclick="hideAbout()">← НАЗАД</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-wrapper" id="game-wrapper" style="display: none;">
|
||||
<div class="game-container">
|
||||
<h1>🚀 ПЛАТФОРМЕР</h1>
|
||||
<canvas id="gameCanvas" width="900" height="550"></canvas>
|
||||
<div class="controls">
|
||||
Управление: <span>←</span> <span>→</span> движение | <span>Пробел</span> прыжок | <span>R</span> перезапуск | <span>Esc</span> пауза
|
||||
</div>
|
||||
<!-- Mobile Controls -->
|
||||
<div class="mobile-controls">
|
||||
<button class="mobile-btn" id="btn-left" ontouchstart="keys.left=true" ontouchend="keys.left=false">◀</button>
|
||||
<button class="mobile-btn" id="btn-jump" ontouchstart="keys.up=true" ontouchend="keys.up=false">⬆️</button>
|
||||
<button class="mobile-btn" id="btn-right" ontouchstart="keys.right=true" ontouchend="keys.right=false">▶</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hud">
|
||||
<div class="hud-group">
|
||||
<div class="hud-item">
|
||||
<span class="label">❤️</span>
|
||||
<div class="lives-display" id="lives"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-display" id="level">УРОВЕНЬ 1: НАЧАЛО</div>
|
||||
<div class="hud-group">
|
||||
<div class="hud-item">
|
||||
<span class="label">💰</span>
|
||||
<span class="value gold" id="score">0</span>
|
||||
</div>
|
||||
<div class="hud-item">
|
||||
<span class="label">⏱️</span>
|
||||
<span class="value" id="timer">0</span>
|
||||
</div>
|
||||
<div class="hud-item combo-display" id="combo">
|
||||
<span class="combo-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="powerups">
|
||||
<div class="powerup-badge" id="powerup-double">
|
||||
<span class="icon">⬆️</span> Двойной прыжок
|
||||
</div>
|
||||
<div class="powerup-badge" id="powerup-speed">
|
||||
<span class="icon">⚡</span> Ускорение
|
||||
</div>
|
||||
<div class="powerup-badge" id="powerup-invincibility">
|
||||
<span class="icon">🛡️</span> Неуязвимость
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="message">
|
||||
<h2 id="messageTitle">🎉 ПОБЕДА!</h2>
|
||||
<div class="stats" id="messageStats"></div>
|
||||
<button onclick="handleMessageButton()">Играть снова</button>
|
||||
</div>
|
||||
|
||||
<div id="pause-menu">
|
||||
<h2>⏸️ ПАУЗА</h2>
|
||||
<button onclick="togglePause(); gameRunning=true;">Продолжить</button>
|
||||
<button onclick="restartGame()">Начать заново</button>
|
||||
<button onclick="exitToMenu()">В главное меню</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Scripts -->
|
||||
<script src="js/sound.js"></script>
|
||||
<script src="js/entities.js"></script>
|
||||
<script src="js/levels.js"></script>
|
||||
<script src="js/game.js"></script>
|
||||
|
||||
<script>
|
||||
// Hide loading, show menu when ready
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('main-menu').style.display = 'flex';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Keyboard shortcut to start game
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (document.getElementById('main-menu').style.display !== 'none') {
|
||||
if (e.code === 'Space' || e.code === 'Enter') {
|
||||
startGame();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user