const http = require("http"); const fs = require("fs"); const path = require("path"); const PORT = process.env.PORT || 3000; const DATA_FILE = path.join(__dirname, "gameState.json"); const DEFAULT_STATE = { part: 1, chapter: "intro", inGameTime: "02:00", playerPosition: { x: 5, y: 5 }, playerRoom: "bedroom", monsterPosition: { x: 15, y: 3 }, monsterRoom: "outside", monsterVisibility: 5, monsterAggression: 0, doorClosed: false, doorLocked: true, lightsOn: false, flashlightOn: false, phoneLightOn: false, wifiOn: true, powerOn: true, panicLevel: 0, messages: [ { from: "СИСТЕМА", text: "Начало игры. Вы дома одни.", time: "02:00", read: true } ], unreadMessages: 0, monsterBehavior: { checkDoor: 0, checkBed: 0, checkLight: 0, checkCamera: 0, checkCloset: 0, checkBathroom: 0, adaptivity: 0.5, memoryIndex: {} }, cameraViews: {}, inventory: [], fusesCollected: 0, fusesNeeded: 3, recorderNotes: [], discoveredSecrets: [], houseDistortion: 0, loopCount: 0, ending: null, endingChoice: null, phonesFound: 0, totalPhones: 12, radioFrequency: 0, correctFrequency: 73.5, alexeyRevealed: false, mirrorMode: false, rooftopReached: false, transmitterDestroyed: false, neighborsExplored: { 145: false, 146: false, 147: false }, fuseLocations: [ { room: "bathroom", collected: false }, { room: "neighbor_146", collected: false }, { room: "stairwell_5", collected: false } ], hidingSpots: [], monsterLastCheck: {}, staircaseDirection: "down", currentFloor: 7, radioHint: "73 point 5", endingA: false, endingB: false, endingC: false }; function loadState() { try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); } catch (_) { return JSON.parse(JSON.stringify(DEFAULT_STATE)); } } function saveState(state) { fs.writeFileSync(DATA_FILE, JSON.stringify(state, null, 2)); } function resetState() { const state = JSON.parse(JSON.stringify(DEFAULT_STATE)); saveState(state); return state; } function body(req) { return new Promise((resolve) => { let s = ""; req.on("data", (c) => (s += c)); req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } }); }); } function updateMonster(state, playerAction, playerRoom, playerPosition) { const behavior = state.monsterBehavior; if (playerAction) { if (playerAction === "check_door") behavior.checkDoor++; if (playerAction === "hide_bed" || playerAction === "hide_under") { behavior.checkBed++; behavior.memoryIndex.lastHide = "bed"; } if (playerAction === "hide_closet") { behavior.checkCloset++; behavior.memoryIndex.lastHide = "closet"; } if (playerAction === "hide_bathroom") { behavior.checkBathroom++; behavior.memoryIndex.lastHide = "bathroom"; } if (playerAction === "toggle_light") behavior.checkLight++; if (playerAction === "use_camera") behavior.checkCamera++; if (playerAction === "use_flashlight") behavior.checkFlashlight++; if (playerAction === "panic") behavior.panicLevel++; } behavior.adaptivity = Math.min(1, behavior.adaptivity + 0.02); if (behavior.checkDoor > 3) behavior.monsterAggression += 0.15; if (behavior.checkBed > 2) behavior.monsterAggression += 0.2; if (behavior.checkCloset > 2) behavior.monsterAggression += 0.2; if (behavior.checkLight > 4) behavior.monsterAggression += 0.1; if (behavior.checkCamera > 3) behavior.monsterAggression += 0.25; state.monsterVisibility = Math.min(100, state.monsterVisibility + (behavior.monsterAggression * 3)); state.panicLevel = Math.min(100, state.panicLevel + (behavior.monsterAggression * 2)); if (state.lightsOn && behavior.checkLight > 2) { state.monsterRoom = "shadows"; } if (!state.lightsOn && !state.flashlightOn && !state.phoneLightOn) { if (Math.random() < 0.25 * behavior.monsterAggression) { state.monsterRoom = playerRoom === "corridor" ? "corridor_end" : "corridor"; } } if (behavior.checkBed > 3 && Math.random() < 0.5) { state.monsterRoom = "bedroom_corner"; } if (behavior.checkCloset > 3 && Math.random() < 0.5) { state.monsterRoom = "closet_near"; } if (state.part >= 4 && Math.random() < 0.3) { const lastHide = behavior.memoryIndex.lastHide; if (lastHide) { state.monsterRoom = lastHide === "bed" ? "bedroom_corner" : lastHide === "closet" ? "closet_near" : "bathroom_door"; } } return state; } function advancePlot(state, trigger, data = {}) { const chapter = state.chapter; const part = state.part; if (part === 1) { if (chapter === "intro" && trigger === "start_routine") { state.messages.push({ from: "СИСТЕМА", text: "Вы дома одни. Родители уехали до утра. 02:00 ночи.", time: "02:00", read: false }); state.chapter = "routine"; } if (chapter === "routine" && trigger === "check_phone") { state.messages.push({ from: "Лёха", text: "Ты тоже это слышишь?", time: "02:14", read: false }); state.unreadMessages++; state.chapter = "message_received"; state.inGameTime = "02:14"; } if (chapter === "message_received" && trigger === "look_window") { state.messages.push({ from: "Лёха", text: "...", time: "02:15", read: false }); state.unreadMessages++; state.chapter = "light_across"; } if (chapter === "light_across" && trigger === "see_silhouette") { state.messages.push({ from: "СИСТЕМА", text: "В квартире напротив, которая пуста 3 года, горит свет...", time: "02:18", read: false }); state.chapter = "power_outage"; state.inGameTime = "02:18"; } if (chapter === "power_outage" && trigger === "lights_out") { state.lightsOn = false; state.powerOn = false; state.wifiOn = false; state.messages.push({ from: "СИСТЕМА", text: "Свет погас. Wi-Fi пропал. Только телефон светит.", time: "02:20", read: false }); state.chapter = "self_message"; } if (chapter === "self_message" && trigger === "door_knock") { state.messages.push({ from: "ВАШ НОМЕР", text: "НЕ ОТКРЫВАЙ ДВЕРЬ.", time: "02:25", read: false, special: true }); state.unreadMessages++; state.chapter = "monster_inside"; } if (chapter === "monster_inside" && trigger === "second_message") { state.messages.push({ from: "ВАШ НОМЕР", text: "Я УЖЕ ВНУТРИ.", time: "02:25", read: false, special: true }); state.unreadMessages++; state.monsterRoom = "inside_house"; state.chapter = "part1_climax"; } if (chapter === "part1_climax" && trigger === "door_unlock") { state.doorLocked = false; state.part = 2; state.chapter = "part2_start"; state.messages.push({ from: "СИСТЕМА", text: "ЧАСТЬ 2: Аномальный тамбур. Нужно выбраться из квартиры.", time: "02:30", read: false }); } } if (part === 2) { if (chapter === "part2_start" && trigger === "explore_tambour") { state.messages.push({ from: "СИСТЕМА", text: "Тамбур затоплен. Виден свет из квартиры 146 (баба Люба).", time: "02:32", read: false }); state.chapter = "neighbor_146"; } if (chapter === "part2_start" && trigger === "enter_146") { state.neighborsExplored[146] = true; state.messages.push({ from: "СИСТЕМА", text: "Квартира 146. На стенах вырезки: 20 лет исчезновений подростков.", time: "02:35", read: false }); state.chapter = "tv_static"; } if (chapter === "neighbor_146" && trigger === "find_fuse") { state.fusesCollected++; state.inventory.push("Предохранитель #1"); state.messages.push({ from: "СИСТЕМА", text: `Найден предохранитель (${state.fusesCollected}/${state.fusesNeeded}). Нужно ещё ${state.fusesNeeded - state.fusesCollected}.`, time: "02:37", read: false }); if (state.fusesCollected >= state.fusesNeeded) { state.chapter = "fusebox"; } } if (chapter === "fusebox" && trigger === "activate_fusebox") { state.powerOn = true; state.lightsOn = true; state.messages.push({ from: "СИСТЕМА", text: "Свет включился! В конце коридора... что-то стоит.", time: "02:45", read: false }); state.monsterRoom = "corridor_end"; state.monsterVisibility = 40; state.chapter = "part2_climax"; } if (chapter === "part2_climax" && trigger === "monster_approach") { state.part = 3; state.chapter = "part3_start"; state.messages.push({ from: "СИСТЕМА", text: "ЧАСТЬ 3: Зацикленный пролёт. Лестница ведёт обратно...", time: "02:50", read: false }); } } if (part === 3) { if (chapter === "part3_start" && trigger === "run_down") { state.messages.push({ from: "СИСТЕМА", text: "Вы бежите вниз. 7 этаж... 6... 5... 1... 7?! Вы снова на своём этаже.", time: "02:52", read: false }); state.loopCount++; state.currentFloor = 7; state.chapter = "loop_realization"; } if (chapter === "loop_realization" && trigger === "check_phone_call") { const fakeCalls = [ { from: "Мама", text: "Данил, выйди на балкон и прыгай! Я поймаю!" }, { from: "МЧС", text: "Запрись в мусоропроводе. Помощь едет." }, { from: "Лёха", text: "Встретимся на крыше. Я жду." } ]; const call = fakeCalls[Math.floor(Math.random() * fakeCalls.length)]; state.messages.push({ from: call.from + " (ЗВОНОК)", text: call.text, time: "02:55", read: false, special: true }); state.unreadMessages++; state.chapter = "spatial_anomaly"; } if (chapter === "spatial_anomaly" && trigger === "check_map") { state.houseDistortion = 0.6; state.messages.push({ from: "СИСТЕМА", text: "КАРТА ИЗМЕНИЛАСЬ! Лифтовая шахта исчезла. На её месте — КВАРТИРА 148.", time: "03:00", read: false, special: true }); state.chapter = "find_148"; } if (chapter === "find_148" && trigger === "enter_148") { state.part = 4; state.chapter = "part4_start"; state.messages.push({ from: "СИСТЕМА", text: "ЧАСТЬ 4: Изнутри. Квартира 148 — это сам дом.", time: "03:05", read: false }); } } if (part === 4) { if (chapter === "part4_start" && trigger === "explore_rooms") { state.messages.push({ from: "СИСТЕМА", text: "Комнаты ведут в другие места: школа, больница, детский сад... Это сон.", time: "03:08", read: false }); state.chapter = "phone_room"; } if (chapter === "phone_room" && trigger === "find_phones") { state.phonesFound++; if (state.phonesFound >= 3) { state.chapter = "alexey_truth"; } state.messages.push({ from: "СИСТЕМА", text: `Найден телефон (${state.phonesFound}/${state.totalPhones}). На них все те же сообщения...`, time: "03:15", read: false }); } if (chapter === "phone_room" && trigger === "find_alexey_chat") { state.alexeyRevealed = true; state.messages.push({ from: "СИСТЕМА", text: "ПЕРЕПИСКА 2018: Лёха пропал 8 лет назад. Сущность использовала его профиль.", time: "03:20", read: false, special: true }); state.chapter = "radio_puzzle"; } if (chapter === "radio_puzzle" && trigger === "set_frequency") { const freq = data.frequency || 0; if (Math.abs(freq - state.correctFrequency) < 1) { state.radioFrequency = state.correctFrequency; state.chapter = "secret_door"; state.messages.push({ from: "СИСТЕМА", text: "Частота настроена! Герметичная дверь открывается...", time: "03:30", read: false }); } else { state.messages.push({ from: "СИСТЕМА", text: "Шум. Нужно точнее. Подсказка: " + state.radioHint, time: "03:28", read: false }); } } if (chapter === "secret_door" && trigger === "enter_center") { state.part = 5; state.chapter = "part5_start"; state.messages.push({ from: "СИСТЕМА", text: "ЧАСТЬ 5: Обратная сторона глазка. Финал.", time: "03:35", read: false }); } } if (part === 5) { if (chapter === "part5_start" && trigger === "mirror_reality") { state.mirrorMode = true; state.messages.push({ from: "СИСТЕМА", text: "Вы в своей комнате... но всё ЗЕРКАЛЬНО. 02:14. Круг замкнулся.", time: "02:14", read: false }); state.chapter = "final_call"; } if (chapter === "final_call" && trigger === "video_call") { state.messages.push({ from: "ВИДЕОЗВОНОК", text: "На экране ВЫ СТОИТЕ посреди комнаты и смотрите на себя.", time: "??", read: false, special: true }); state.unreadMessages++; state.chapter = "ending_choice"; } if (chapter === "ending_choice" && trigger === "choice_a") { state.ending = "A"; state.endingA = true; state.messages.push({ from: "КОНЦОВКА А: СИГНАЛ", text: "Вы на крыше. Шаг в туман. Новое SMS: 'Ты тоже это слышишь?' отправлено следующему.", time: "∞", read: false, special: true, ending: true }); } if (chapter === "ending_choice" && trigger === "choice_b") { state.ending = "B"; state.endingB = true; state.transmitterDestroyed = true; state.messages.push({ from: "КОНЦОВКА Б: СБРОС", text: "Передатчик уничтожен. 08:00. Родители вернулись. Но в окне напротив КТО-ТО СМОТРИТ.", time: "08:00", read: false, special: true, ending: true }); } if (chapter === "ending_choice" && trigger === "choice_c") { state.ending = "C"; state.endingC = true; state.messages.push({ from: "КОНЦОВКА В: НОВЫЙ ЖИЛЕЦ", text: "Вы садитесь за стол в кв.148. Берёте телефон. Печатаете: 'Ты тоже это слышишь?'", time: "∞", read: false, special: true, ending: true }); } } return state; } http.createServer(async (req, res) => { const url = req.url.split("?")[0]; if (req.method === "GET" && url === "/api/state") { res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(loadState())); } if (req.method === "POST" && url === "/api/reset") { const state = resetState(); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(state)); } if (req.method === "POST" && url === "/api/action") { const data = await body(req); let state = loadState(); state = updateMonster(state, data.action, state.playerRoom, state.playerPosition); state = advancePlot(state, data.trigger, data); if (data.move) { state.playerPosition.x += data.move.x || 0; state.playerPosition.y += data.move.y || 0; } if (data.action === "toggle_door") { state.doorClosed = !state.doorClosed; if (!state.doorClosed) state.doorLocked = false; } if (data.action === "toggle_light") { state.lightsOn = !state.lightsOn; } if (data.action === "toggle_flashlight") { state.flashlightOn = !state.flashlightOn; } if (data.action === "toggle_phone_light") { state.phoneLightOn = !state.phoneLightOn; } if (data.action === "collect_fuse") { state.fusesCollected++; state.inventory.push(`Предохранитель #${state.fusesCollected}`); } if (data.action === "find_phone") { state.phonesFound++; } if (data.action === "record_sound") { state.recorderNotes.push({ time: state.inGameTime, sound: data.sound || "аномальный шум", analyzed: false }); } saveState(state); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(state)); } if (req.method === "POST" && url === "/api/camera") { const data = await body(req); let state = loadState(); const cameraId = data.cameraId; const visibility = state.monsterVisibility; const aggression = state.monsterAggression; let monsterVisible = false; let monsterData = null; if (cameraId === "phone" && state.monsterRoom === state.playerRoom) { if (Math.random() * 100 < visibility * (1 + aggression)) { monsterVisible = true; monsterData = { type: "silhouette", position: "background" }; } } if (cameraId === "entrance" && (state.monsterRoom === "corridor" || state.monsterRoom === "entrance")) { if (Math.random() * 100 < visibility * 0.7 * (1 + aggression)) { monsterVisible = true; monsterData = { type: "shadow", position: "stairs" }; } } if (cameraId === "across" && state.monsterRoom === "apartment_across") { if (Math.random() * 100 < visibility * 0.5 * (1 + aggression)) { monsterVisible = true; monsterData = { type: "figure", position: "window" }; } } if (cameraId === "stairwell" && state.part === 3) { if (Math.random() * 100 < visibility * 0.8) { monsterVisible = true; monsterData = { type: "multiple", position: "all_floors" }; } } state.cameraViews[cameraId] = { timestamp: Date.now(), monsterVisible, monsterData, noise: Math.random() * 0.3 + state.houseDistortion * 0.3 + aggression * 0.2, distortion: state.houseDistortion }; saveState(state); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(state.cameraViews[cameraId])); } if (req.method === "POST" && url === "/api/recorder") { const data = await body(req); let state = loadState(); state.recorderNotes.push({ time: state.inGameTime, sound: data.sound || "неизвестный шум", frequency: data.frequency || 0, analyzed: data.analyzed || false }); saveState(state); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(state.recorderNotes)); } let file = url === "/" ? "/index.html" : url; const full = path.join(__dirname, file); if (fs.existsSync(full) && fs.statSync(full).isFile()) { const ext = path.extname(full); const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); return res.end(fs.readFileSync(full)); } res.writeHead(404); res.end("Not found"); }).listen(PORT, () => console.log("🎮 НОЧНОЙ ДОМ запущен на порту " + PORT));