mozhesh-sozdat-igru-vot-stse/index.html

1395 lines
45 KiB
HTML
Raw Permalink 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, maximum-scale=1, user-scalable=no">
<title>НОЧНОЙ ДОМ</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: #050508;
color: #d0d0d0;
font-family: 'Courier New', monospace;
overflow: hidden;
touch-action: none;
}
#game {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
#hud {
position: fixed;
top: 10px;
left: 10px;
z-index: 50;
font-size: 12px;
color: #888;
}
.hud-item {
background: rgba(0,0,0,0.6);
padding: 5px 10px;
border-radius: 4px;
margin-bottom: 5px;
border: 1px solid rgba(100,100,100,0.2);
}
#part-indicator {
position: fixed;
top: 10px;
right: 10px;
background: rgba(50,0,0,0.7);
padding: 8px 15px;
border-radius: 6px;
font-size: 11px;
border: 1px solid rgba(200,50,50,0.3);
z-index: 50;
}
#ui {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 12px;
background: linear-gradient(to top, #000 0%, transparent 100%);
z-index: 100;
}
.controls {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
background: rgba(20, 20, 30, 0.85);
border: 1px solid rgba(80, 80, 100, 0.4);
color: #b0b0b0;
padding: 10px 14px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
backdrop-filter: blur(3px);
min-width: 70px;
}
.btn:hover, .btn:active {
background: rgba(35, 35, 50, 0.9);
border-color: rgba(120, 120, 150, 0.6);
transform: scale(1.05);
}
.btn.active {
background: rgba(80, 40, 40, 0.7);
border-color: rgba(180, 80, 80, 0.6);
color: #ff8888;
}
.btn-danger {
background: rgba(60, 20, 20, 0.8);
border-color: rgba(150, 50, 50, 0.5);
}
#phone {
position: fixed;
bottom: -100%;
left: 50%;
transform: translateX(-50%);
width: min(380px, 95vw);
height: min(650px, 75vh);
background: linear-gradient(145deg, #1a1a28 0%, #0d0d18 100%);
border: 2px solid #444;
border-radius: 28px;
z-index: 200;
transition: bottom 0.35s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
box-shadow: 0 -15px 50px rgba(0,0,0,0.8), inset 0 0 30px rgba(0,0,0,0.5);
}
#phone.open {
bottom: 0;
}
#phone-header {
background: linear-gradient(to bottom, #2a2a38, #1a1a28);
padding: 18px 15px;
text-align: center;
border-bottom: 1px solid #3a3a4a;
font-size: 15px;
font-weight: bold;
color: #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
#phone-signal {
font-size: 11px;
color: #666;
}
#phone-content {
height: calc(100% - 120px);
overflow-y: auto;
padding: 15px;
background: rgba(10, 10, 15, 0.5);
}
.message {
background: rgba(35, 35, 50, 0.7);
border-left: 3px solid #666;
padding: 14px;
margin-bottom: 14px;
border-radius: 0 10px 10px 0;
font-size: 13px;
line-height: 1.5;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(-20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.message.special {
border-left-color: #c00;
background: rgba(70, 25, 25, 0.65);
animation: pulse 2.5s infinite, slideIn 0.3s ease;
}
.message.ending {
border-left-color: #f0f;
background: linear-gradient(135deg, rgba(80,20,80,0.7), rgba(40,10,40,0.8));
font-size: 14px;
}
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 10px rgba(200,50,50,0.3); }
50% { opacity: 0.85; box-shadow: 0 0 20px rgba(200,50,50,0.6); }
}
.message-from {
color: #999;
font-size: 11px;
margin-bottom: 6px;
font-weight: bold;
}
.message-time {
color: #666;
font-size: 10px;
float: right;
}
#phone-tabs {
display: flex;
background: #121218;
border-top: 1px solid #333;
position: absolute;
bottom: 0;
width: 100%;
}
.tab {
flex: 1;
padding: 14px 10px;
text-align: center;
background: transparent;
border: none;
color: #666;
cursor: pointer;
font-size: 11px;
border-right: 1px solid #2a2a35;
transition: all 0.2s;
}
.tab.active {
background: rgba(60, 60, 90, 0.5);
color: #d0d0e0;
}
.camera-view {
background: #000;
border: 2px solid #333;
border-radius: 10px;
margin-bottom: 12px;
overflow: hidden;
position: relative;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
}
.camera-view canvas {
width: 100%;
height: 160px;
display: block;
}
.camera-label {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0,0,0,0.75);
padding: 4px 10px;
border-radius: 5px;
font-size: 10px;
color: #0f0;
font-weight: bold;
border: 1px solid rgba(0,255,0,0.2);
}
.camera-rec {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
background: #f00;
border-radius: 50%;
animation: blink 0.8s infinite;
box-shadow: 0 0 8px #f00;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.25; }
}
.recorder-view {
background: linear-gradient(145deg, #1a1a1a, #0a0a0a);
border: 2px solid #444;
border-radius: 10px;
padding: 15px;
margin-bottom: 12px;
}
.recorder-display {
background: #000;
border: 1px solid #333;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
font-family: 'Digital', monospace;
font-size: 24px;
color: #f00;
text-align: center;
letter-spacing: 3px;
}
.frequency-control {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.frequency-control input {
flex: 1;
background: #222;
border: 1px solid #444;
color: #0f0;
padding: 8px;
border-radius: 4px;
font-family: monospace;
}
#close-phone {
position: fixed;
top: 15px;
right: 15px;
background: rgba(0,0,0,0.8);
border: 2px solid #666;
color: #fff;
width: 44px;
height: 44px;
border-radius: 50%;
font-size: 26px;
cursor: pointer;
z-index: 201;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
}
#close-phone.visible {
display: flex;
}
#story-text {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
padding: 18px 28px;
border-radius: 12px;
max-width: 90vw;
text-align: center;
z-index: 50;
font-size: 14px;
line-height: 1.7;
border: 1px solid rgba(120,120,120,0.3);
display: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
}
#story-text.visible {
display: block;
animation: fadeIn 0.6s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.scanline {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.12),
rgba(0, 0, 0, 0.12) 1px,
transparent 1px,
transparent 2px
);
}
.vignette {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.7) 100%);
z-index: 1001;
}
.noise {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.04;
z-index: 999;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%25" height="100%25" filter="url(%23n)"/></svg>');
animation: noiseAnim 0.5s steps(10) infinite;
}
@keyframes noiseAnim {
0%, 100% { transform: translate(0, 0); }
20% { transform: translate(-5%, -5%); }
40% { transform: translate(5%, 5%); }
60% { transform: translate(-5%, 5%); }
80% { transform: translate(5%, -5%); }
}
#jumpscare {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 3000;
display: none;
align-items: center;
justify-content: center;
}
#jumpscare.visible {
display: flex;
animation: shake 0.3s infinite;
}
@keyframes shake {
0%, 100% { transform: translate(0, 0); }
25% { transform: translate(-10px, 10px); }
50% { transform: translate(10px, -10px); }
75% { transform: translate(-10px, -10px); }
}
#jumpscare canvas {
max-width: 100%;
max-height: 100%;
}
#panic-bar {
position: fixed;
bottom: 100px;
right: 20px;
width: 6px;
height: 150px;
background: rgba(30,30,30,0.8);
border: 1px solid #444;
border-radius: 3px;
z-index: 80;
overflow: hidden;
}
#panic-fill {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 0%;
background: linear-gradient(to top, #0066cc, #00ccff);
transition: height 0.5s;
}
#panic-fill.high {
background: linear-gradient(to top, #cc3300, #ff6600);
}
#panic-label {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
font-size: 9px;
color: #888;
white-space: nowrap;
}
.ending-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 4000;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
}
.ending-screen.visible {
display: flex;
}
.ending-title {
font-size: 28px;
color: #f0f;
margin-bottom: 20px;
text-shadow: 0 0 20px #f0f;
}
.ending-text {
font-size: 16px;
color: #ccc;
line-height: 1.8;
max-width: 600px;
}
.ending-credits {
margin-top: 40px;
font-size: 12px;
color: #666;
}
#inventory {
position: fixed;
bottom: 120px;
left: 20px;
background: rgba(0,0,0,0.7);
border: 1px solid #444;
border-radius: 8px;
padding: 10px;
z-index: 70;
max-width: 200px;
display: none;
}
#inventory.visible {
display: block;
}
.inventory-item {
font-size: 11px;
color: #aaa;
padding: 4px 0;
border-bottom: 1px solid #333;
}
.inventory-item:last-child {
border-bottom: none;
}
#chapter-title {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
color: #fff;
text-align: center;
z-index: 60;
display: none;
text-shadow: 0 0 30px rgba(200,50,50,0.8);
animation: chapterFade 3s forwards;
}
#chapter-title.visible {
display: block;
}
@keyframes chapterFade {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.2); }
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div class="scanline"></div>
<div class="vignette"></div>
<div class="noise"></div>
<div id="hud">
<div class="hud-item" id="time-display">⏰ 02:00</div>
<div class="hud-item" id="room-display">📍 Квартира</div>
<div class="hud-item" id="power-display">⚡ Свет: ВЫКЛ</div>
</div>
<div id="part-indicator">
<div id="part-text">ЧАСТЬ 1</div>
<div id="chapter-text" style="font-size:9px;color:#888;margin-top:4px;">Вспышка напротив</div>
</div>
<div id="story-text"></div>
<div id="chapter-title"></div>
<div id="panic-bar">
<div id="panic-fill"></div>
<div id="panic-label">ПАНИКА</div>
</div>
<div id="inventory">
<div style="font-size:10px;color:#888;margin-bottom:6px;">📦 ИНВЕНТАРЬ</div>
<div id="inventory-list"></div>
</div>
<div id="ui">
<div class="controls">
<button class="btn" id="btn-door">🚪 Дверь</button>
<button class="btn" id="btn-light">💡 Свет</button>
<button class="btn" id="btn-flashlight">🔦 Фонарь</button>
<button class="btn" id="btn-hide">🙈 Спрятаться</button>
<button class="btn" id="btn-phone">📱 Телефон</button>
<button class="btn" id="btn-look">👁️ Осмотр</button>
<button class="btn" id="btn-recorder" style="display:none;">🎤 Диктофон</button>
</div>
</div>
<div id="phone">
<div id="phone-header">
<span>📱 СМАРТФОН</span>
<span id="phone-signal">📶 Нет сети</span>
</div>
<div id="phone-content">
<div id="tab-messages"></div>
<div id="tab-cameras" style="display:none;"></div>
<div id="tab-map" style="display:none;"></div>
<div id="tab-recorder" style="display:none;"></div>
</div>
<div id="phone-tabs">
<button class="tab active" data-tab="messages">💬 Сообщения</button>
<button class="tab" data-tab="cameras">📷 Камеры</button>
<button class="tab" data-tab="map">🗺 Карта</button>
<button class="tab" data-tab="recorder" style="display:none;">🎤 Диктофон</button>
</div>
</div>
<div id="close-phone">✕</div>
<div id="jumpscare">
<canvas id="jumpscare-canvas"></canvas>
</div>
<div id="ending-screen" class="ending-screen">
<div class="ending-title" id="ending-title"></div>
<div class="ending-text" id="ending-text"></div>
<div class="ending-credits">
<p>🎮 НОЧНОЙ ДОМ</p>
<p>Хоррор с адаптивным ИИ</p>
<button class="btn" onclick="location.reload()" style="margin-top:20px;">🔄 Начать заново</button>
</div>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let gameState = null;
let currentTab = 'messages';
let hideSpot = null;
let frequency = 70.0;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
async function loadState() {
try {
const res = await fetch('/api/state');
gameState = await res.json();
render();
updateHUD();
} catch(e) {
console.error('Failed to load state:', e);
}
}
async function sendAction(action, trigger = null, data = {}) {
try {
const res = await fetch('/api/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, trigger, ...data })
});
gameState = await res.json();
render();
updateHUD();
return gameState;
} catch(e) {
console.error('Failed to send action:', e);
return null;
}
}
async function checkCamera(cameraId) {
try {
const res = await fetch('/api/camera', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cameraId })
});
return await res.json();
} catch(e) {
return { error: true };
}
}
function updateHUD() {
if (!gameState) return;
document.getElementById('time-display').textContent = '⏰ ' + gameState.inGameTime;
document.getElementById('room-display').textContent = '📍 ' + getRoomName(gameState.playerRoom);
document.getElementById('power-display').textContent = '⚡ Свет: ' + (gameState.lightsOn ? 'ВКЛ' : 'ВЫКЛ');
document.getElementById('part-text').textContent = 'ЧАСТЬ ' + gameState.part;
document.getElementById('chapter-text').textContent = getChapterName(gameState.chapter);
const panicFill = document.getElementById('panic-fill');
panicFill.style.height = gameState.panicLevel + '%';
panicFill.className = gameState.panicLevel > 70 ? 'high' : '';
const inventoryEl = document.getElementById('inventory');
const inventoryList = document.getElementById('inventory-list');
if (gameState.inventory.length > 0) {
inventoryEl.classList.add('visible');
inventoryList.innerHTML = gameState.inventory.map(item =>
'<div class="inventory-item">' + item + '</div>'
).join('');
} else {
inventoryEl.classList.remove('visible');
}
document.getElementById('btn-recorder').style.display = gameState.part >= 3 ? 'block' : 'none';
document.querySelector('[data-tab="recorder"]').style.display = gameState.part >= 3 ? 'block' : 'none';
}
function getRoomName(room) {
const names = {
'bedroom': 'Спальня',
'corridor': 'Коридор',
'kitchen': 'Кухня',
'living': 'Гостиная',
'bathroom': 'Ванная',
'tambour': 'Тамбур',
'neighbor_145': 'Кв. 145',
'neighbor_146': 'Кв. 146',
'neighbor_147': 'Кв. 147',
'stairwell': 'Лестница',
'apartment_148': 'Кв. 148',
'phone_room': 'Комната телефонов',
'mirror_room': 'Зеркальная комната',
'rooftop': 'Крыша'
};
return names[room] || room;
}
function getChapterName(chapter) {
const names = {
'intro': 'Начало',
'routine': 'Рутина',
'message_received': 'Сообщение',
'light_across': 'Свет напротив',
'power_outage': 'Отключение',
'monster_inside': 'ОНО внутри',
'part2_start': 'Тамбур',
'neighbor_146': 'Кв. 146',
'fusebox': 'Щиток',
'part3_start': 'Петля',
'loop_realization': 'Зациклен',
'part4_start': 'Изнутри',
'phone_room': 'Телефоны',
'part5_start': 'Финал',
'ending_choice': 'Выбор'
};
return names[chapter] || chapter;
}
function drawCharacter(x, y, isPlayer = true, isMonster = false) {
ctx.save();
ctx.translate(x, y);
if (isMonster) {
const size = 35 + Math.sin(Date.now() / 150) * 5;
const alpha = Math.min(gameState.monsterVisibility / 100, 0.85);
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size * 1.5);
gradient.addColorStop(0, `rgba(60, 0, 0, ${alpha * 0.9})`);
gradient.addColorStop(0.4, `rgba(30, 0, 0, ${alpha * 0.5})`);
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.ellipse(0, 0, size * 0.6, size, 0, 0, Math.PI * 2);
ctx.fill();
if (gameState.monsterVisibility > 25) {
ctx.fillStyle = `rgba(255, 80, 80, ${alpha * 0.8})`;
ctx.beginPath();
const eyeOffset = Math.sin(Date.now() / 300) * 3;
ctx.arc(-10, -10 + eyeOffset, 4 + Math.random() * 2, 0, Math.PI * 2);
ctx.arc(10, -10 + eyeOffset, 4 + Math.random() * 2, 0, Math.PI * 2);
ctx.fill();
}
if (gameState.monsterVisibility > 60) {
ctx.strokeStyle = `rgba(255, 50, 50, ${alpha * 0.5})`;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-15, 15);
ctx.quadraticCurveTo(-5, 5, 0, 15);
ctx.quadraticCurveTo(5, 5, 15, 15);
ctx.stroke();
}
} else if (isPlayer) {
const size = 22;
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size);
gradient.addColorStop(0, '#7a9cff');
gradient.addColorStop(1, '#3a5a9f');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(0, 0, size, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(-6, -4, 3.5, 0, Math.PI * 2);
ctx.arc(6, -4, 3.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(-6, -4, 1.5, 0, Math.PI * 2);
ctx.arc(6, -4, 1.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawRoom(room, centerX, centerY, width, height) {
const gradients = {
'bedroom': ['#1a1a3e', '#16214e'],
'corridor': ['#0f0f1a', '#1a1a2e'],
'kitchen': ['#1a1510', '#2e2515'],
'living': ['#151530', '#202040'],
'bathroom': ['#101518', '#152528'],
'tambour': ['#0a0a15', '#151525'],
'neighbor_146': ['#252015', '#353020'],
'stairwell': ['#080810', '#181828'],
'apartment_148': ['#1a0a0a', '#2a1010'],
'phone_room': ['#1a1a1a', '#2a2a2a'],
'mirror_room': ['#1a0a1a', '#2a102a'],
'rooftop': ['#050510', '#101020']
};
const colors = gradients[room] || ['#1a1a1a', '#2a2a2a'];
const gradient = ctx.createLinearGradient(centerX - width/2, centerY - height/2, centerX + width/2, centerY + height/2);
gradient.addColorStop(0, colors[0]);
gradient.addColorStop(1, colors[1]);
ctx.fillStyle = gradient;
ctx.fillRect(centerX - width/2, centerY - height/2, width, height);
if (gameState.houseDistortion > 0) {
ctx.save();
ctx.globalAlpha = gameState.houseDistortion * 0.3;
ctx.strokeStyle = `rgba(255, 0, 0, 0.2)`;
ctx.lineWidth = 2;
ctx.setLineDash([5, 10]);
ctx.strokeRect(centerX - width/2 + 10, centerY - height/2 + 10, width - 20, height - 20);
ctx.restore();
}
ctx.strokeStyle = 'rgba(100, 100, 120, 0.25)';
ctx.lineWidth = 2;
ctx.strokeRect(centerX - width/2, centerY - height/2, width, height);
if (room === 'bedroom') {
ctx.fillStyle = '#2a2a4e';
ctx.fillRect(centerX - width/2 + 25, centerY + height/2 - 70, width - 50, 50);
ctx.fillStyle = '#3a3a5e';
ctx.fillRect(centerX - width/2 + 25, centerY + height/2 - 70, 35, 28);
}
if (room === 'neighbor_146') {
ctx.fillStyle = 'rgba(60, 50, 40, 0.5)';
for (let i = 0; i < 8; i++) {
ctx.fillRect(centerX - width/2 + 20 + i * 25, centerY - height/2 + 20, 20, 30);
}
}
if (room === 'phone_room') {
ctx.fillStyle = '#1a1a1a';
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 3; j++) {
ctx.fillRect(centerX - 60 + i * 40, centerY - 40 + j * 35, 30, 25);
}
}
}
}
function render() {
if (!gameState) return;
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const roomWidth = Math.min(canvas.width * 0.85, 450);
const roomHeight = Math.min(canvas.height * 0.65, 350);
drawRoom(gameState.playerRoom, centerX, centerY, roomWidth, roomHeight);
const playerX = centerX + (gameState.playerPosition.x - 5) * 25;
const playerY = centerY + (gameState.playerPosition.y - 5) * 25;
if (gameState.phoneLightOn || gameState.flashlightOn) {
const lightGradient = ctx.createRadialGradient(playerX, playerY, 15, playerX, playerY, 180);
lightGradient.addColorStop(0, 'rgba(255, 255, 220, 0.35)');
lightGradient.addColorStop(1, 'transparent');
ctx.fillStyle = lightGradient;
ctx.fillRect(playerX - 180, playerY - 180, 360, 360);
}
drawCharacter(playerX, playerY, true);
if (gameState.monsterRoom === gameState.playerRoom ||
gameState.monsterRoom === 'shadows' ||
gameState.monsterRoom === 'inside_house' ||
gameState.monsterRoom === 'corridor') {
const monsterX = gameState.monsterRoom === gameState.playerRoom ?
playerX + (Math.random() - 0.5) * 180 :
centerX + (Math.random() - 0.5) * roomWidth;
const monsterY = gameState.monsterRoom === gameState.playerRoom ?
playerY + (Math.random() - 0.5) * 130 :
centerY + (Math.random() - 0.5) * roomHeight;
drawCharacter(monsterX, monsterY, false, true);
}
if (gameState.lightsOn) {
ctx.fillStyle = 'rgba(255, 255, 220, 0.08)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
const darkness = gameState.lightsOn ? 0.25 : (gameState.flashlightOn || gameState.phoneLightOn ? 0.45 : 0.75);
ctx.fillStyle = `rgba(0, 0, 0, ${darkness})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (gameState.mirrorMode) {
ctx.save();
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
ctx.fillStyle = 'rgba(255, 0, 255, 0.03)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
}
function renderPhone() {
if (!gameState) return;
document.getElementById('phone-signal').textContent = gameState.wifiOn ? '📶 4G' : '📴 Нет сети';
if (currentTab === 'messages') {
const container = document.getElementById('tab-messages');
if (gameState.messages.length === 0) {
container.innerHTML = '<div style="color:#555;text-align:center;padding:40px;">Нет сообщений</div>';
} else {
container.innerHTML = gameState.messages.map(m => `
<div class="message${m.special ? ' special' : ''}${m.ending ? ' ending' : ''}">
<div class="message-from">${m.from}<span class="message-time">${m.time}</span></div>
<div>${m.text}</div>
</div>
`).reverse().join('');
}
}
if (currentTab === 'cameras') {
const container = document.getElementById('tab-cameras');
const cams = gameState.part >= 2 ? ['phone', 'entrance', 'across'] : ['phone'];
if (gameState.part >= 3) cams.push('stairwell');
container.innerHTML = cams.map(cam => `
<div class="camera-view" data-cam="${cam}">
<div class="camera-label">${getCamName(cam)}</div>
<div class="camera-rec"></div>
<canvas id="cam-${cam}" height="160"></canvas>
</div>
`).join('');
setTimeout(() => {
cams.forEach(async (cam) => {
const data = await checkCamera(cam);
const c = document.getElementById(`cam-${cam}`);
if (c) {
const cx = c.getContext('2d');
c.width = c.offsetWidth;
for (let i = 0; i < c.width; i += 3) {
for (let j = 0; j < 160; j += 3) {
const noise = Math.random() * 25 + data.noise * 80;
const green = Math.floor(noise);
cx.fillStyle = `rgb(0, ${green}, 0)`;
cx.fillRect(i, j, 3, 3);
}
}
if (data.monsterVisible && data.monsterData) {
cx.fillStyle = 'rgba(80, 0, 0, 0.75)';
cx.beginPath();
const mx = c.width / 2 + (Math.random() - 0.5) * 80;
const my = 80 + (Math.random() - 0.5) * 50;
cx.ellipse(mx, my, 30, 55, 0, 0, Math.PI * 2);
cx.fill();
cx.fillStyle = '#f00';
cx.beginPath();
cx.arc(mx - 12, my - 18, 5, 0, Math.PI * 2);
cx.arc(mx + 12, my - 18, 5, 0, Math.PI * 2);
cx.fill();
}
}
});
}, 150);
}
if (currentTab === 'map') {
const container = document.getElementById('tab-map');
container.innerHTML = renderMap();
}
if (currentTab === 'recorder') {
const container = document.getElementById('tab-recorder');
container.innerHTML = `
<div class="recorder-view">
<div class="recorder-display">${frequency.toFixed(1)} MHz</div>
<div class="frequency-control">
<button class="btn" onclick="adjustFreq(-1)">-</button>
<input type="range" min="60" max="90" step="0.1" value="${frequency}" oninput="adjustFreq(this.value)">
<button class="btn" onclick="adjustFreq(1)">+</button>
</div>
<button class="btn" onclick="setFrequency()" style="width:100%;margin-top:12px;">📻 Настроить частоту</button>
<div style="margin-top:15px;font-size:11px;color:#888;">
Записей: ${gameState.recorderNotes.length}
</div>
</div>
`;
}
}
function getCamName(cam) {
const names = {
'phone': 'ТЕЛЕФОН',
'entrance': 'ПОДЪЕЗД',
'across': 'НАПРОТИВ',
'stairwell': 'ЛЕСТНИЦА'
};
return names[cam] || cam;
}
function renderMap() {
if (gameState.part === 1) {
return `
<div style="padding:20px;text-align:center;color:#999;">
<p>🗺 ПЛАН КВАРТИРЫ</p>
<div style="margin-top:20px;border:2px solid #555;padding:15px;background:#111;">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;max-width:250px;margin:0 auto;">
<div style="background:#252540;padding:15px;text-align:center;font-size:11px;">Ванна</div>
<div style="background:${gameState.playerRoom === 'bedroom' ? '#4a4a8a' : '#1a1a3e'};padding:15px;text-align:center;font-size:11px;">${gameState.playerRoom === 'bedroom' ? '📍ВЫ' : 'Спальня'}</div>
<div style="background:#252535;padding:15px;text-align:center;font-size:11px;">Кухня</div>
<div style="background:#151525;padding:15px;text-align:center;font-size:11px;grid-column:span 3;">КОРИДОР</div>
<div style="background:#252540;padding:15px;text-align:center;font-size:11px;">Гостиная</div>
<div style="background:#1a1a2e;padding:15px;text-align:center;font-size:11px;">Дверь</div>
</div>
</div>
</div>
`;
}
if (gameState.part === 2) {
return `
<div style="padding:20px;text-align:center;color:#999;">
<p>🗺 ТАМБУР И КВАРТИРЫ</p>
<div style="margin-top:15px;font-size:11px;">
<div style="padding:8px;background:${gameState.neighborsExplored[146] ? '#353020' : '#1a1a1a'};margin:5px;">🚪 Кв. 146 (баба Люба) ${gameState.neighborsExplored[146] ? '✅' : ''}</div>
<div style="padding:8px;background:#1a1a1a;margin:5px;">🚪 Кв. 145 (закрыто)</div>
<div style="padding:8px;background:#1a1a1a;margin:5px;">🚪 Кв. 147 (закрыто)</div>
<div style="padding:8px;background:#151525;margin:5px;">⚡ Щиток: ${gameState.fusesCollected}/${gameState.fusesNeeded}</div>
</div>
</div>
`;
}
if (gameState.part === 3) {
return `
<div style="padding:20px;text-align:center;color:#999;">
<p>🗺 ЛЕСТНИЦА (ПЕТЛЯ)</p>
<div style="margin-top:15px;font-size:11px;">
<div style="padding:8px;background:${gameState.currentFloor === 7 ? '#3a3a5a' : '#1a1a1a'};margin:5px;">${gameState.currentFloor === 7 ? '📍' : ''} 7 этаж (ваш)</div>
<div style="padding:8px;background:#1a1a1a;margin:5px;">6 этаж</div>
<div style="padding:8px;background:#1a1a1a;margin:5px;">5 этаж</div>
<div style="padding:8px;background:#1a1a1a;margin:5px;">...</div>
<div style="padding:8px;background:#2a1a1a;margin:5px;color:#f88;">⚠️ КВАРТИРА 148</div>
</div>
<p style="margin-top:15px;font-size:10px;color:#f66;">⚠️ Пространство искажено</p>
</div>
`;
}
return `
<div style="padding:20px;text-align:center;color:#999;">
<p>🗺 КАРТА НЕДОСТУПНА</p>
<p style="font-size:11px;margin-top:10px;">Сигнал потерян</p>
</div>
`;
}
function adjustFreq(val) {
frequency = parseFloat(val);
const display = document.querySelector('.recorder-display');
if (display) display.textContent = frequency.toFixed(1) + ' MHz';
}
async function setFrequency() {
await sendAction('set_frequency', null, { frequency });
renderPhone();
}
document.getElementById('btn-door').addEventListener('click', async () => {
const state = await sendAction('toggle_door', gameState.doorClosed ? 'door_open' : 'door_close');
if (state && state.chapter === 'part1_climax') {
await sendAction(null, 'door_unlock');
}
});
document.getElementById('btn-light').addEventListener('click', async () => {
await sendAction('toggle_light', 'toggle_light');
});
document.getElementById('btn-flashlight').addEventListener('click', async () => {
await sendAction('toggle_flashlight', 'use_flashlight');
});
document.getElementById('btn-hide').addEventListener('click', async () => {
const spots = ['bed', 'closet', 'bathroom'];
hideSpot = spots[Math.floor(Math.random() * spots.length)];
await sendAction('hide_' + hideSpot);
showStoryText(`Вы спрятались: ${hideSpot === 'bed' ? 'под кровать' : hideSpot === 'closet' ? 'в шкаф' : 'в ванной'}`);
});
document.getElementById('btn-phone').addEventListener('click', async () => {
document.getElementById('phone').classList.add('open');
document.getElementById('close-phone').classList.add('visible');
renderPhone();
if (gameState.chapter === 'intro' || gameState.chapter === 'routine') {
await sendAction(null, 'check_phone');
}
});
document.getElementById('btn-look').addEventListener('click', async () => {
await sendAction('look', 'check_corridor');
if (gameState.chapter === 'light_across') {
await sendAction(null, 'see_silhouette');
}
if (gameState.chapter === 'part2_start') {
await sendAction(null, 'explore_tambour');
}
if (gameState.chapter === 'loop_realization') {
await sendAction(null, 'check_phone_call');
}
if (gameState.chapter === 'spatial_anomaly') {
await sendAction(null, 'check_map');
}
});
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentTab = tab.dataset.tab;
['messages', 'cameras', 'map', 'recorder'].forEach(t => {
document.getElementById('tab-' + t).style.display = currentTab === t ? 'block' : 'none';
});
renderPhone();
});
});
document.getElementById('close-phone').addEventListener('click', () => {
document.getElementById('phone').classList.remove('open');
document.getElementById('close-phone').classList.remove('visible');
});
canvas.addEventListener('click', async (e) => {
if (gameState.chapter === 'monster_inside' || gameState.chapter === 'self_message') {
await sendAction(null, 'door_knock');
setTimeout(async () => {
await sendAction(null, 'second_message');
triggerJumpscare();
}, 1500);
}
if (gameState.chapter === 'neighbor_146') {
await sendAction(null, 'enter_146');
}
if (gameState.chapter === 'part2_start' && gameState.neighborsExplored[146]) {
await sendAction('collect_fuse', 'find_fuse');
}
if (gameState.chapter === 'fusebox' && gameState.fusesCollected >= gameState.fusesNeeded) {
await sendAction(null, 'activate_fusebox');
}
if (gameState.chapter === 'part2_climax') {
await sendAction(null, 'monster_approach');
}
if (gameState.chapter === 'part3_start') {
await sendAction(null, 'run_down');
}
if (gameState.chapter === 'find_148') {
await sendAction(null, 'enter_148');
}
if (gameState.chapter === 'phone_room') {
if (!gameState.alexeyRevealed && Math.random() < 0.5) {
await sendAction('find_phone', 'find_alexey_chat');
} else {
await sendAction('find_phone', 'find_phones');
}
}
if (gameState.chapter === 'secret_door') {
await sendAction(null, 'enter_center');
}
if (gameState.chapter === 'part5_start') {
await sendAction(null, 'mirror_reality');
}
if (gameState.chapter === 'mirror_reality') {
await sendAction(null, 'video_call');
}
if (gameState.chapter === 'ending_choice') {
showEndingChoices();
}
});
function showEndingChoices() {
const choices = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:3500;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;">
<h2 style="color:#f0f;margin-bottom:30px;font-size:24px;">ВЫБЕРИТЕ КОНЦОВКУ</h2>
<button class="btn btn-danger" onclick="chooseEnding('a')" style="margin:10px;padding:15px 30px;font-size:14px;">
А: СИГНАЛ<br><span style="font-size:11px;color:#888;">Сбежать на крышу</span>
</button>
<button class="btn" onclick="chooseEnding('b')" style="margin:10px;padding:15px 30px;font-size:14px;">
Б: СБРОС<br><span style="font-size:11px;color:#888;">Уничтожить передатчик</span>
</button>
<button class="btn" onclick="chooseEnding('c')" style="margin:10px;padding:15px 30px;font-size:14px;">
В: НОВЫЙ ЖИЛЕЦ<br><span style="font-size:11px;color:#888;">Принять судьбу</span>
</button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', choices);
}
window.chooseEnding = async (choice) => {
await sendAction(null, 'choice_' + choice);
document.querySelector('[style*="z-index:3500"]').remove();
showEnding(gameState.ending);
};
function showEnding(ending) {
const screen = document.getElementById('ending-screen');
const title = document.getElementById('ending-title');
const text = document.getElementById('ending-text');
const endings = {
'A': {
title: 'КОНЦОВКА А: СИГНАЛ',
text: 'Вы добираетесь до крыши. Город внизу кажется таким далёким. Вы делаете шаг в туман...\n\nГде-то в доме зазвонил телефон. Новое сообщение отправлено: "Ты тоже это слышишь?"\n\nКруг замкнулся.'
},
'B': {
title: 'КОНЦОВКА Б: СБРОС',
text: 'Вы находите передатчик в квартире 148 и разбиваете его молотком.\n\n08:00. Солнце светит в окно. Родители вернулись, зовут на завтрак. Всё кончено?\n\nВы смотрите в окно напротив. В окне квартиры 148 стоит фигура и машет вам.'
},
'C': {
title: 'КОНЦОВКА В: НОВЫЙ ЖИЛЕЦ',
text: 'Вы понимаете — бежать некуда. Дом стал частью вас.\n\nВы садитесь за стол в квартире 148. Берёте старый кнопочный телефон. Пальцы сами набирают текст:\n\n"Ты тоже это слышишь?"\n\nСообщение отправлено. Ждите гостя.'
}
};
const end = endings[ending];
title.textContent = end.title;
text.textContent = end.text;
screen.classList.add('visible');
}
function triggerJumpscare() {
const js = document.getElementById('jumpscare');
js.classList.add('visible');
const jCanvas = document.getElementById('jumpscare-canvas');
jCanvas.width = window.innerWidth;
jCanvas.height = window.innerHeight;
const jCtx = jCanvas.getContext('2d');
jCtx.fillStyle = '#000';
jCtx.fillRect(0, 0, jCanvas.width, jCanvas.height);
for (let i = 0; i < 600; i++) {
jCtx.fillStyle = `rgba(${Math.random() > 0.5 ? '255' : '0'}, 0, 0, ${Math.random() * 0.6})`;
jCtx.beginPath();
jCtx.arc(Math.random() * jCanvas.width, Math.random() * jCanvas.height, Math.random() * 4 + 1, 0, Math.PI * 2);
jCtx.fill();
}
const gradient = jCtx.createRadialGradient(jCanvas.width/2, jCanvas.height/2, 0, jCanvas.width/2, jCanvas.height/2, 350);
gradient.addColorStop(0, 'rgba(150, 0, 0, 0.9)');
gradient.addColorStop(1, 'transparent');
jCtx.fillStyle = gradient;
jCtx.beginPath();
jCtx.ellipse(jCanvas.width/2, jCanvas.height/2, 220, 320, 0, 0, Math.PI * 2);
jCtx.fill();
jCtx.fillStyle = '#f00';
jCtx.font = 'bold 18px Arial';
for (let i = 0; i < 150; i++) {
jCtx.fillText('•', Math.random() * jCanvas.width, Math.random() * jCanvas.height);
}
setTimeout(() => {
js.classList.remove('visible');
}, 400);
}
function showStoryText(text) {
const el = document.getElementById('story-text');
el.textContent = text;
el.classList.add('visible');
setTimeout(() => el.classList.remove('visible'), 4000);
}
function showChapterTitle(text) {
const el = document.getElementById('chapter-title');
el.textContent = text;
el.classList.add('visible');
setTimeout(() => el.classList.remove('visible'), 3000);
}
document.addEventListener('keydown', (e) => {
if (e.key === 'd' || e.key === 'в') sendAction('toggle_door');
if (e.key === 'l' || e.key === 'с') sendAction('toggle_light');
if (e.key === 'f' || e.key === 'а') sendAction('toggle_flashlight');
if (e.key === 'p' || e.key === 'т') {
document.getElementById('phone').classList.toggle('open');
document.getElementById('close-phone').classList.toggle('visible');
renderPhone();
}
if (e.key === 'h' || e.key === 'р') document.getElementById('btn-hide').click();
if (e.key === 'Escape') {
document.getElementById('phone').classList.remove('open');
document.getElementById('close-phone').classList.remove('visible');
}
});
setInterval(() => {
if (gameState && gameState.chapter === 'intro') {
sendAction(null, 'start_routine');
}
if (gameState && gameState.chapter === 'power_outage') {
sendAction(null, 'lights_out');
}
}, 8000);
loadState();
setInterval(loadState, 3000);
function animate() {
render();
requestAnimationFrame(animate);
}
animate();
setTimeout(() => {
showChapterTitle('ЧАСТЬ 1\nВспышка напротив');
}, 1000);
</script>
</body>
</html>