mozhesh-sozdat-igru-vot-stse/server.js

665 lines
20 KiB
JavaScript
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.

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));