Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Бесконечный путь — эскиз</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #e8e8e8;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Courier New', Courier, monospace;
padding: 20px;
}
.game-container {
background: #fdfdfd;
border-radius: 4px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
canvas {
display: block;
background: #ffffff;
border: 1px solid #ccc;
border-radius: 2px;
cursor: default;
box-shadow: inset 0 0 20px rgba(0,0,0,0.02);
}
.hint {
margin-top: 14px;
font-size: 15px;
color: #555;
letter-spacing: 0.5px;
text-align: center;
user-select: none;
}
.hint kbd {
background: #f0f0f0;
border: 1px solid #bbb;
border-radius: 4px;
padding: 2px 8px;
font-family: inherit;
font-size: 13px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
/* Плашка "Задача" */
.task-panel {
position: absolute;
top: 35px;
left: 35px;
background: #f8f8f8;
border: 1px solid #ccc;
border-radius: 4px;
padding: 12px 18px;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
user-select: none;
pointer-events: none;
z-index: 5;
}
.task-panel .task-title {
font-weight: bold;
font-size: 16px;
color: #333;
margin-bottom: 6px;
letter-spacing: 1px;
}
.task-panel .task-text {
font-size: 14px;
color: #555;
font-style: italic;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="900" height="500"></canvas>
<div class="task-panel">
<div class="task-title">Задача</div>
<div class="task-text">Изучить комнаты</div>
</div>
<div class="hint">
<kbd>←</kbd>/<kbd>A</kbd> — влево | <kbd>→</kbd>/<kbd>D</kbd> — вправо | <kbd>E</kbd> — взаимодействие
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Параметры мира
const GROUND_Y = 390;
const PLAYER_SCREEN_X = 450;
const PLAYER_HEIGHT = 180;
const HEAD_RADIUS = 22;
const BODY_LENGTH = 80;
const LEG_LENGTH = 50;
const MOVE_SPEED = 4.5;
// Состояние
let worldOffsetX = 0;
let isMovingLeft = false;
let isMovingRight = false;
let walkCycle = 0;
let lastTimestamp = 0;
let currentRoom = 'main'; // 'main' или 'room1'
let isNearDoor = false; // Игрок рядом с дверью
// Граница слева
const LEFT_BOUNDARY_DISTANCE = MOVE_SPEED * 60 * 10;
const LEFT_BOUNDARY_WORLD_X = -LEFT_BOUNDARY_DISTANCE;
const TEXT_WORLD_X = LEFT_BOUNDARY_WORLD_X + 150;
const TEXT_WORLD_Y = GROUND_Y - 80;
// Дверь справа
const DOOR_WORLD_X = 1000;
const DOOR_WIDTH = 70;
const DOOR_HEIGHT = 160;
const DOOR_TOP_Y = GROUND_Y - DOOR_HEIGHT;
// Генерация стабильных штрихов для надписи "Прими смерть"
const deathStrokes = [];
const deathDots = [];
(function generateDeathArt() {
for (let i = 0; i < 8; i++) {
deathStrokes.push({
startX: -180 + Math.random() * 360,
startY: -40 + Math.random() * 80,
cp1x: 20 + Math.random() * 60,
cp1y: -10 + Math.random() * 30,
cp2x: 40 + Math.random() * 80,
cp2y: 5 + Math.random() * 20
});
}
for (let i = 0; i < 12; i++) {
deathDots.push({
x: -200 + Math.random() * 400,
y: -45 + Math.random() * 90,
r: Math.random() * 4 + 1
});
}
})();
// Обработчики клавиш
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (currentRoom === 'main') {
if (key === 'arrowleft' || key === 'a') {
isMovingLeft = true;
e.preventDefault();
} else if (key === 'arrowright' || key === 'd') {
isMovingRight = true;
e.preventDefault();
} else if (key === 'e' && isNearDoor) {
enterRoom();
}
} else if (currentRoom === 'room1') {
if (key === 'e') {
exitRoom();
}
}
// Блокируем прокрутку стрелками
if (['arrowleft', 'arrowright', 'a', 'd'].includes(key)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (key === 'arrowleft' || key === 'a') {
isMovingLeft = false;
e.preventDefault();
} else if (key === 'arrowright' || key === 'd') {
isMovingRight = false;
e.preventDefault();
}
});
// Вход в комнату
function enterRoom() {
currentRoom = 'room1';
isMovingLeft = false;
isMovingRight = false;
}
// Выход из комнаты
function exitRoom() {
currentRoom = 'main';
isMovingLeft = false;
isMovingRight = false;
}
// Конвертация мировых координат в экранные
function worldToScreen(worldX) {
return worldX + worldOffsetX + PLAYER_SCREEN_X;
}
// Проверка близости к двери
function checkDoorProximity() {
if (currentRoom !== 'main') {
isNearDoor = false;
return;
}
const playerWorldX = -worldOffsetX;
const distanceToDoor = Math.abs(playerWorldX - DOOR_WORLD_X);
isNearDoor = distanceToDoor < 50;
}
// Рисование надписи "Прими смерть" углём (стабильные штрихи)
function drawDeathMessage() {
if (currentRoom !== 'main') return;
const screenX = worldToScreen(TEXT_WORLD_X);
const screenY = TEXT_WORLD_Y;
if (screenX < -300 || screenX > canvas.width + 300) return;
ctx.save();
ctx.font = 'bold 48px "Courier New", Courier, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Тень
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
ctx.fillText('Прими смерть', screenX + 4, screenY + 6);
// Основной текст
ctx.fillStyle = '#1a1a1a';
ctx.fillText('Прими смерть', screenX, screenY);
// Штрихи углём (стабильные)
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1.5;
for (const stroke of deathStrokes) {
ctx.beginPath();
ctx.moveTo(screenX + stroke.startX, screenY + stroke.startY);
ctx.quadraticCurveTo(
screenX + stroke.startX + stroke.cp1x,
screenY + stroke.startY + stroke.cp1y,
screenX + stroke.startX + stroke.cp2x,
screenY + stroke.startY + stroke.cp2y
);
ctx.stroke();
}
// Точки-помарки (стабильные)
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
for (const dot of deathDots) {
ctx.beginPath();
ctx.arc(screenX + dot.x, screenY + dot.y, dot.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// Рисование двери
function drawDoor() {
if (currentRoom !== 'main') return;
const screenX = worldToScreen(DOOR_WORLD_X);
if (screenX < -100 || screenX > canvas.width + 100) return;
ctx.save();
// Дверная коробка
ctx.fillStyle = '#8b7355';
ctx.fillRect(screenX - DOOR_WIDTH/2, DOOR_TOP_Y, DOOR_WIDTH, DOOR_HEIGHT);
// Дверное полотно
ctx.fillStyle = '#a0845c';
ctx.fillRect(screenX - DOOR_WIDTH/2 + 5, DOOR_TOP_Y + 5, DOOR_WIDTH - 10, DOOR_HEIGHT - 5);
// Ручка
ctx.fillStyle = '#c0a060';
ctx.beginPath();
ctx.arc(screenX + DOOR_WIDTH/2 - 15, GROUND_Y - DOOR_HEIGHT/2, 5, 0, Math.PI * 2);
ctx.fill();
// Подсветка при приближении
if (isNearDoor) {
ctx.fillStyle = 'rgba(255, 255, 200, 0.25)';
ctx.fillRect(screenX - DOOR_WIDTH/2 - 5, DOOR_TOP_Y - 5, DOOR_WIDTH + 10, DOOR_HEIGHT + 5);
ctx.font = 'bold 14px "Courier New", Courier, monospace';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText('[E] Войти', screenX, DOOR_TOP_Y - 15);
}
ctx.restore();
}
// Рисование линии пола и границы
function drawInfiniteGround() {
if (currentRoom !== 'main') return;
ctx.save();
// Линия пола
ctx.beginPath();
ctx.moveTo(-2000, GROUND_Y);
ctx.lineTo(2000, GROUND_Y);
ctx.strokeStyle = '#2c2c2c';
ctx.lineWidth = 2.5;
ctx.stroke();
// Граница слева
const boundaryScreenX = worldToScreen(LEFT_BOUNDARY_WORLD_X);
if (boundaryScreenX > -10 && boundaryScreenX < canvas.width + 10) {
ctx.beginPath();
ctx.moveTo(boundaryScreenX, GROUND_Y - 200);
ctx.lineTo(boundaryScreenX, GROUND_Y + 50);
ctx.strokeStyle = '#2c2c2c';
ctx.lineWidth = 2;
ctx.setLineDash([8, 6]);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(0, 0, 0, 0.03)';
ctx.fillRect(boundaryScreenX, 0, canvas.width - boundaryScreenX, GROUND_Y);
}
// Деления
const spacing = 80;
const startMark = Math.floor(-worldOffsetX / spacing) * spacing;
for (let x = startMark - spacing; x < canvas.width + spacing; x += spacing) {
const screenX = x + worldOffsetX + PLAYER_SCREEN_X;
if (screenX < -20 || screenX > canvas.width + 20) continue;
ctx.beginPath();
ctx.moveTo(screenX, GROUND_Y - 10);
ctx.lineTo(screenX, GROUND_Y + 10);
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1.2;
ctx.stroke();
}
ctx.restore();
}
// Рисование пустой комнаты
function drawRoom() {
ctx.save();
// Белый фон
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Пол
ctx.beginPath();
ctx.moveTo(0, GROUND_Y);
ctx.lineTo(canvas.width, GROUND_Y);
ctx.strokeStyle = '#2c2c2c';
ctx.lineWidth = 2.5;
ctx.stroke();
// Стены
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(50, GROUND_Y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(canvas.width - 50, 50);
ctx.lineTo(canvas.width - 50, GROUND_Y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(canvas.width - 50, 50);
ctx.stroke();
// Надпись
ctx.font = 'italic 20px "Courier New", Courier, monospace';
ctx.fillStyle = '#aaa';
ctx.textAlign = 'center';
ctx.fillText('Пустая комната', canvas.width / 2, GROUND_Y - 150);
// Подсказка выхода
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(canvas.width / 2 - 60, GROUND_Y + 50, 120, 40);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.strokeRect(canvas.width / 2 - 60, GROUND_Y + 50, 120, 40);
ctx.fillStyle = '#555';
ctx.font = '14px "Courier New", Courier, monospace';
ctx.fillText('Выйти [E]', canvas.width / 2, GROUND_Y + 75);
ctx.restore();
}
// Рисование человечка (главный мир)
function drawPlayer() {
if (currentRoom !== 'main') return;
ctx.save();
const playerX = PLAYER_SCREEN_X;
let bounce = 0;
if (isMovingLeft || isMovingRight) {
bounce = Math.sin(walkCycle * 2) * 1.5;
}
const headY = GROUND_Y - PLAYER_HEIGHT + HEAD_RADIUS + bounce;
const neckY = headY + HEAD_RADIUS + 2;
const hipY = neckY + BODY_LENGTH;
const footY = GROUND_Y;
const legSwing = (isMovingLeft || isMovingRight) ? Math.sin(walkCycle) * 12 : 0;
// Левая нога
const leftFootX = playerX - 8 + legSwing * 0.8;
ctx.beginPath();
ctx.moveTo(playerX - 4, hipY);
ctx.lineTo(playerX - 5, hipY + LEG_LENGTH * 0.55);
ctx.lineTo(leftFootX, footY);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2.8;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// Правая нога
const rightFootX = playerX + 8 - legSwing * 0.8;
ctx.beginPath();
ctx.moveTo(playerX + 4, hipY);
ctx.lineTo(playerX + 5, hipY + LEG_LENGTH * 0.55);
ctx.lineTo(rightFootX, footY);
ctx.stroke();
// Туловище
ctx.beginPath();
ctx.moveTo(playerX, neckY);
ctx.lineTo(playerX, hipY);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 3.5;
ctx.stroke();
// Руки
const armSwing = (isMovingLeft || isMovingRight) ? Math.sin(walkCycle + Math.PI) * 8 : 0;
ctx.beginPath();
ctx.moveTo(playerX - 1, neckY + 12);
ctx.lineTo(playerX - 14 + armSwing * 0.6, neckY + 38);
ctx.lineTo(playerX - 18 + armSwing * 0.9, neckY + 56);
ctx.lineWidth = 2.2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(playerX + 1, neckY + 12);
ctx.lineTo(playerX + 14 - armSwing * 0.6, neckY + 38);
ctx.lineTo(playerX + 18 - armSwing * 0.9, neckY + 56);
ctx.stroke();
// Голова
ctx.beginPath();
ctx.arc(playerX, headY, HEAD_RADIUS, 0, Math.PI * 2);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2.5;
ctx.stroke();
// Глаза
const lookDirection = (isMovingLeft && !isMovingRight) ? -1 : (isMovingRight && !isMovingLeft) ? 1 : 0;
const eyeOffsetX = lookDirection * 3;
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.arc(playerX - 6 + eyeOffsetX, headY - 2, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(playerX + 6 + eyeOffsetX, headY - 2, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Обновление состояния
function update() {
if (currentRoom === 'main') {
const movingLeft = isMovingLeft && !isMovingRight;
const movingRight = isMovingRight && !isMovingLeft;
if (movingLeft) {
const newOffset = worldOffsetX + MOVE_SPEED;
const newPlayerWorldX = -newOffset;
if (newPlayerWorldX > LEFT_BOUNDARY_WORLD_X + 10) {
worldOffsetX = newOffset;
} else {
worldOffsetX = -(LEFT_BOUNDARY_WORLD_X + 10);
}
}
if (movingRight) {
worldOffsetX -= MOVE_SPEED;
}
if (movingLeft || movingRight) {
walkCycle += 0.18;
} else {
walkCycle *= 0.85;
}
checkDoorProximity();
}
}
// Отрисовка
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentRoom === 'main') {
// Белый лист
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Лёгкая текстура бумаги
ctx.fillStyle = 'rgba(0,0,0,0.008)';
for (let i = 0; i < 20; i++) {
ctx.beginPath();
ctx.arc(
100 + Math.random() * 700,
80 + Math.random() * 350,
Math.random() * 30 + 10,
0, Math.PI * 2
);
ctx.fill();
}
drawInfiniteGround();
drawDeathMessage();
drawDoor();
drawPlayer();
} else if (currentRoom === 'room1') {
drawRoom();