1395 lines
45 KiB
HTML
1395 lines
45 KiB
HTML
<!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>
|