diff --git a/db2.js b/db2.js new file mode 100644 index 0000000..aec4e1d --- /dev/null +++ b/db2.js @@ -0,0 +1,48 @@ +require('dotenv').config(); +const { Pool } = require('pg'); + +// Проверяем наличие строки подключения +if (!process.env.DATABASE_QUEST_NEW_QUESTS) { + console.error('❌ Ошибка: DATABASE_QUEST_NEW_QUESTS не задана в .env файле'); + throw new Error('DATABASE_QUEST_NEW_QUESTS environment variable is required'); +} + +const connectionStr = process.env.DATABASE_QUEST_NEW_QUESTS; +console.log('Подключение к базе данных new_quests'); + +// Проверяем, содержит ли строка подключения пароль +if (!connectionStr.includes(':')) { + console.error('❌ Ошибка: Строка подключения не содержит пароль'); + throw new Error('Database connection string must include password'); +} + +const new_quest_Base = new Pool({ + connectionString: connectionStr, + ssl: false +}); + +// Обработчики событий +new_quest_Base.on('error', (err) => { + console.error('❌ Ошибка подключения к базе данных new_quests:', err.message); +}); + +new_quest_Base.on('connect', () => { + console.log('✅ Успешное подключение к базе данных new_quests'); +}); + +// Функция для проверки подключения +new_quest_Base.testConnection = async () => { + try { + await new_quest_Base.query('SELECT 1 as test'); + console.log('✅ Тест подключения к new_quests успешен'); + return true; + } catch (error) { + console.error('❌ Тест подключения к new_quests failed:', error.message); + return false; + } +}; + +module.exports = { + query: (text, params) => new_quest_Base.query(text, params), + testConnection: () => new_quest_Base.testConnection() +}; \ No newline at end of file diff --git a/public/dialogs/Adventurer.json b/public/dialogs/Adventurer.json index 4be8f80..871f063 100644 --- a/public/dialogs/Adventurer.json +++ b/public/dialogs/Adventurer.json @@ -1,150 +1,30 @@ { - "name": "Галина", - "avatar": "/images/npc/bartender.jpg", + "name": "Искатель приключений", + "avatar": "/images/npc/adventurer.jpg", "filename": "Adventurer.json", + "npc_id": 3, + "dialogue_key": "adventurer/advice", "dialog": [ { "id": 0, - "text": "Ну? Значит, ты к нам за работой припёрся? Ладно, давай документы...", + "text": "А, новое лицо! Ищешь работу? Я бы на твоем месте сначала поговорил со стражником у входа. Он всем новичкам дает наводки.", "answers": [ { - "text": "Документов нет", - "next": 1 - }, - { - "text": "Я просто осматриваюсь...", - "next": 2 - }, - { - "text": "...", + "text": "Спасибо за совет", "end": true + }, + { + "text": "А что ты знаешь о работе?", + "next": 1 } ] }, { "id": 1, - "text": "Ах, у тебя их нет? Я бы удивилась другому развитию событий. Тогда придется заполнить их. Писать то хоть умеешь?", - "answers": [ - { - "text": "Как-нибудь справлюсь", - "next": "form_data" - }, - { - "text": "Может, есть работа без документов?", - "next": 3 - } - ] - }, - { - "id": 2, - "text": "Осматриваешься? Ну-ну... Только не задерживайся слишком долго. В нашем городе без дела болтаться — себе дороже.", - "answers": [ - { - "text": "А куда можно пойти?", - "next": 3 - }, - { - "text": "Ладно, тогда давай документы...", - "next": 1 - } - ] - }, - { - "id": 3, - "text": "Слушай, есть один парень... сидит в кустах за углом. Зовут его Костя Ключник. Он как раз набирает людей для одной работы. Только не говори, что я тебя направила.", - "answers": [ - { - "text": "Спасибо, попробую", - "next": 4 - }, - { - "text": "А что за работа?", - "next": 5 - } - ] - }, - { - "id": 4, - "text": "Ну иди уже, не задерживайся. И смотри в оба — Костя любит шутки, но не над собой.", - "answers": [ - { - "end": true - } - ] - }, - { - "id": 5, - "text": "Если бы я знала все детали, сама бы там работала. Иди сам узнаешь. Если, конечно, не струсишь.", - "answers": [ - { - "text": "Ладно, иду", - "next": 4 - }, - { - "text": "Может, лучше заполню документы?", - "next": "form_data" - } - ] - }, - { - "id": "form_data", - "type": "form", - "title": "Заполнение анкеты", - "fields": [ - { - "name": "skills", - "label": "Твои навыки и перки:", - "type": "text", - "placeholder": "Навыки через запятую", - "required": true - }, - { - "name": "work_experience", - "label": "Предыдущие места работы:", - "type": "textarea", - "placeholder": "Где и кем работал", - "required": false - }, - { - "name": "background", - "label": "Твое прошлое:", - "type": "textarea", - "placeholder": "Краткая информация о себе", - "required": false - } - ], - "submit_text": "Отправить данные", - "next": 6 - }, - { - "id": 6, - "text": "Так... посмотрим что тут... (долго листает бумаги) Ну ты и говно, дружок. Ладно, есть для тебя один вариант.", - "answers": [ - { - "text": "Какой?", - "next": 7 - } - ] - }, - { - "id": 7, - "text": "Слушай сюда. Есть один парень... сидит в кустах за углом. Зовут его Костя Ключник. Он как раз набирает людей. Вот и вся вакансия.", - "answers": [ - { - "text": "И это всё?", - "next": 8 - }, - { - "text": "Ладно, пойду", - "next": 4 - } - ] - }, - { - "id": 8, - "text": "Да, это всё. Ты думал, у нас тут офис белых воротничков? Иди уже, не задерживайся.", + "text": "Я-то? Да много чего... Но сначала пройди базовый инструктаж. Без этого тебя никто серьезный не возьмет.", "answers": [ { + "text": "Понял, спасибо", "end": true } ] diff --git a/public/dialogs/BeachCharacter.json b/public/dialogs/BeachCharacter.json index 5f98c49..b85d6c0 100644 --- a/public/dialogs/BeachCharacter.json +++ b/public/dialogs/BeachCharacter.json @@ -1,167 +1,47 @@ { - "name": "Костя Ключник", - "avatar": "/images/npc/guard.jpg", + "name": "Пляжный персонаж", + "avatar": "/images/npc/beach_character.jpg", "filename": "BeachCharacter.json", + "npc_id": 4, + "dialogue_key": "beachcharacter/meet", "dialog": [ { "id": 0, - "text": "(нервно озираясь по сторонам) Тссс... Ты кто такой? Мент? А? Нет? Ну ладно... (быстро чешет нос) Галина говорила? Ну эта... которая в баре...", + "text": "Эй, приятель! Ты выглядишь как человек, который ищет приключений. У меня есть одно дельце...", "answers": [ { - "text": "Да, она направила", + "text": "Какое дело?", "next": 1 }, { - "text": "Кто такая Галина?", - "next": 2 + "text": "Извини, я спешу", + "end": true } ] }, { "id": 1, - "text": "(хихикает) Ага, ну конечно направила... Она у нас вся такая... (внезапно серьёзнеет) Ладно, браток, работа есть. Но сначала... (ковыряет в носу) Ты че, стремаешься?", + "text": "Нужно кое-что доставить в старую часть города. Опасно, но платят хорошо. Заинтересован?", "answers": [ { - "text": "Какая работа?", - "next": 3 + "text": "Расскажи подробнее", + "next": 2 }, { - "text": "Ты че такой странный?", - "next": 4 + "text": "Слишком рискованно", + "end": true } ] }, { "id": 2, - "text": "(параноидально оглядывается) Ты че, прикалываешься? Галина! Ну... (делает жест рукой у рта) Которая... Ну в общем... (внезапно меняет тему) Ты хочешь заработать или нет?", - "answers": [ - { - "text": "Хочу заработать", - "next": 3 - }, - { - "text": "Ты точно нормальный?", - "next": 4 - } - ] - }, - { - "id": 3, - "text": "(потирает руки) Оооо, братан, работа огонь! Цифры взламывать будем! (внезапно замолкает, прислушивается) Ты слышал? Нет? Ну ладно... (быстро) Но сначала... (достаёт из кармана пакетик) Хочешь попробовать? Для смелости...", - "answers": [ - { - "text": "Давай попробую", - "next": 5 - }, - { - "text": "Нет, я не употребляю", - "next": 6 - } - ] - }, - { - "id": 4, - "text": "(нервно смеётся) Нормальный? Да я самый нормальный тут! (вдруг серьёзнеет) Вот только вчера... Нет, не буду рассказывать... (глаза бегают) Ты работу хочешь или нет?", - "answers": [ - { - "text": "Хочу работу", - "next": 3 - }, - { - "text": "Ты пугаешь меня", - "next": 7 - } - ] - }, - { - "id": 5, - "text": "(радостно) Ооо, наш человек! (суёт пакетик) На вот, только не всё сразу, а то... (делает широкий жест руками) Бах! И тебя нет! Ха-ха! Ладно, заходи завтра, когда... ну... разберёшься со своими делами. Я тебе всё расскажу.", - "answers": [ - { - "text": "Ладно, приду завтра", - "next": 8, - "quest_start": "hack_job" - }, - { - "text": "Может лучше прямо сейчас?", - "next": 9 - } - ] - }, - { - "id": 6, - "text": "(разочарованно) Фу, скукота... (пожимает плечами) Ну ладно, работа всё равно есть. Заходи завтра, я тебе всё расскажу. Только... (понижает голос) Никому не говори, ладно?", - "answers": [ - { - "text": "Хорошо, приду завтра", - "next": 8, - "quest_start": "hack_job" - }, - { - "text": "А можно подробнее?", - "next": 10 - } - ] - }, - { - "id": 7, - "text": "(внезапно злится) Пугаю? Да я тебя... (резко успокаивается) Ладно, братан, иди отсюда. Не для тебя эта работа. (начинает что-то бормотать себе под нос)", + "text": "Встреться с моим контактом у заброшенного дока. Скажешь, что от Кости. Деньги получишь по завершению.", "answers": [ { + "text": "Договорились", "end": true } ] - }, - { - "id": 8, - "text": "(кивает) Молодец. Запомни: подвал за углом, охраннику скажешь... (шёпотом) 'берёзовый сок'. Он тебя пропустит. И... (внезапно хватает за руку) Только никому, понял? Ни-ко-му!", - "answers": [ - { - "end": true - } - ] - }, - { - "id": 9, - "text": "(панически) Сейчас? Нет, нет, нет! (осматривается) Слишком много... глаз. Завтра. Только завтра. (начинает быстро уходить)", - "answers": [ - { - "end": true - } - ] - }, - { - "id": 10, - "text": "(нервно оглядывается) Подробнее? Ну... (понижает голос) Есть подвал. Там компы. Надо... ну... (делает движение пальцами как при печати) Взламывать. Охраннику скажешь 'берёзовый сок' - пропустит. Всё. Больше ничего не знаю. (начинает чесаться)", - "answers": [ - { - "text": "Понятно, приду завтра", - "next": 8, - "quest_start": "hack_job" - }, - { - "text": "Это незаконно!", - "next": 11 - } - ] - }, - { - "id": 11, - "text": "(истерично смеётся) Законно? Ха! В этом городе? (внезапно серьёзнеет) Ладно, иди отсюда, мальчик. Ищи себе 'законную' работу. (поворачивается спиной)", - "answers": [ - { - "end": true - } - ] - }, - { - "id": "hack_job", - "type": "quest", - "title": "Взлом в подвале", - "description": "Нужно проникнуть в подвал, сказав охраннику пароль 'берёзовый сок', и взломать данные", - "location": "/locations/basement.json", - "reward": "500 кредитов", - "next": 8 } ] } \ No newline at end of file diff --git a/public/dialogs/Oxranik.json b/public/dialogs/Oxranik.json index d326732..f81890c 100644 --- a/public/dialogs/Oxranik.json +++ b/public/dialogs/Oxranik.json @@ -2,29 +2,43 @@ "name": "Охранник", "avatar": "/images/npc/guard.jpg", "filename": "Oxranik.json", + "npc_id": 5, + "dialogue_key": "oxranik/report", "dialog": [ { "id": 0, - "text": "Стоять! Кто такой?", + "text": "Стоять! Что нужно?", "answers": [ { - "text": "Березовый сок", - "next": 1, - "required_quest": "hack_job" + "text": "Я выполнил задания", + "next": 1 }, { - "text": "Я ошибся дверью", + "text": "Ничего, ошибся", "end": true } ] }, { "id": 1, - "text": "(кивает) Проходи. Но предупреждаю - если что-то пойдет не так, я тебя не знал.", + "text": "Так... Вижу, ты поработал. Неплохо для новичка. Держи награду и приходи за новыми заданиями завтра.", "answers": [ { - "text": "Понял", - "quest_progress": "hack_job", + "text": "Спасибо", + "end": true + }, + { + "text": "Что дальше?", + "next": 2 + } + ] + }, + { + "id": 2, + "text": "Отдохни сегодня. Завтра будут новые поручения. Спроси у бармена - он знает.", + "answers": [ + { + "text": "Понял, до завтра", "end": true } ] diff --git a/public/dialogs/bartender.json b/public/dialogs/bartender.json index 7229050..f3aea2a 100644 --- a/public/dialogs/bartender.json +++ b/public/dialogs/bartender.json @@ -1,42 +1,26 @@ { - "name": "Серега Пират", + "name": "Бармен", "avatar": "/images/npc/bartender.jpg", "filename": "bartender.json", + "npc_id": 2, + "dialogue_key": "bartender/dialogue", "dialog": [ { "id": 0, - "text": "Ну что, дружок, застрял как муха в паутине? Или просто решил проверить, насколько крепки эти стены?", + "text": "Привет, новичок. Вижу, ты уже поговорил со стражником. Ну что, готов к настоящей работе?", "answers": [ { - "text": "Я... кажется, ошибся дверью.", - "end": true - }, - { - "text": "Мне сказали, тут можно «устроиться». От Галины.", - "next": 2 + "text": "Расскажи, что есть", + "next": 1 } ] }, { - "id": 2, - "text": "Ага, Галка-весточка. Слушай сюда: правила простые — садишься за комп, выполняешь два задания. Первое — по звукам угадаешь пароль. Второе — в финансовых документах найдёшь косяк. Справишься — получишь свои кровные. Не справишься... ну, сам понимаешь.", + "id": 1, + "text": "У меня есть пара контактов. Но сначала послушай того искателя приключений в углу - у него есть полезная информация для новичков.", "answers": [ { - "text": "И где этот ваш комп?", - "next": 3 - }, - { - "text": "А сложно будет?", - "next": 3 - } - ] - }, - { - "id": 3, - "text": "*кивает на потрёпанный системник в углу* Там всё включено. Разберёшься. Главное — уши навостри и глаза пошире открой. Как будешь готов — жми любую кнопку.", - "answers": [ - { - "text": "*Подойти к компьютеру*", + "text": "Хорошо, поговорю с ним", "end": true } ] diff --git a/public/dialogs/guard.json b/public/dialogs/guard.json index 93da3ac..8f08422 100644 --- a/public/dialogs/guard.json +++ b/public/dialogs/guard.json @@ -1,102 +1,26 @@ { - "name": "Саша Белый", + "name": "Стражник", "avatar": "/images/npc/guard.jpg", "filename": "guard.json", + "npc_id": 1, + "dialogue_key": "guard/intro", "dialog": [ { "id": 0, - "text": "А вот и новенький подъехал… Чё, глаза такие круглые? Добро пожаловать в Realternity – Moscow City, братан. Тут не экскурсия, так что уши на макушке держи.", + "text": "Стой! Новенький? Добро пожаловать в наш город. Первое правило - хочешь выжить, ищи работу.", "answers": [ { - "text": "Понял...", - "next": 1 - }, - { - "text": "Что это за место?", + "text": "Где можно найти работу?", "next": 1 } ] }, { "id": 1, - "text": "Город у нас большой, светится неоном, как новогодняя ёлка, но под этой мишурой — помойка, крысы да волки. Вверх глянешь — небоскрёбы корпораций, вниз — подземка, где людей за карточку еды режут. Каждый тут сам за себя.", + "text": "Начни с таверны. Бармен всегда в курсе, кому нужны руки. Иди к нему.", "answers": [ { - "text": "И что делать?", - "next": 2 - }, - { - "text": "Звучит жутко...", - "next": 2 - } - ] - }, - { - "id": 2, - "text": "Первое правило — хочешь жить, ищи работу. Деньги — это воздух. Без них ты тут не человек, а мусор под ногами.", - "answers": [ - { - "text": "А если работы нет?", - "next": 3 - }, - { - "text": "Где искать?", - "next": 4 - } - ] - }, - { - "id": 3, - "text": "Нет работы? Ну, значит, ищешь... альтернативы. Тут таких путей — как тараканов в общаге. Главное — не ной, действуй.", - "answers": [ - { - "text": "Какие альтернативы?", - "next": 5 - }, - { - "text": "Понятно...", - "next": 6 - } - ] - }, - { - "id": 4, - "text": "Короче, если совсем не врубаешься, топай в Центр Трудоустройства «Нижний Эшелон». Там помогут... ну, если не кинут. Смотри в оба, братан.", - "answers": [ - { - "text": "Спасибо за совет", - "next": 6 - }, - { - "text": "А где это?", - "next": 6 - } - ] - }, - { - "id": 5, - "text": "Честный ты, нечестный — неважно. Главное — чтоб ты выбрал, по какой дорожке топать. По свету, по тени… или, может, будешь тем, кто идёт посередине и стреляет в обе стороны. Тут за всё платят — вопрос только, чем.", - "answers": [ - { - "text": "Ясно...", - "next": 6 - }, - { - "text": "Страшновато", - "next": 6 - } - ] - }, - { - "id": 6, - "text": "И запомни: здесь не детский сад. Никто за ручку водить не будет. Или ты адаптируешься… или сгниёшь на подворотне. Всё просто.", - "answers": [ - { - "text": "Понял, спасибо", - "end": true - }, - { - "text": "До встречи", + "text": "Спасибо, пойду в таверну", "end": true } ] diff --git a/server.js b/server.js index e389a94..bdb3ce7 100644 --- a/server.js +++ b/server.js @@ -33,7 +33,7 @@ catch (e) { console.error('Ошибка при импорте db1 - virtual_World:', e); throw e; } -/* + try { new_quest_Base = require('./db2'); console.log('db2 - new_quest_Base - успешно импортирован'); @@ -42,7 +42,7 @@ catch (e) { console.error('Ошибка при импорте db2 - new_quest_Base: ', e); throw e; } -*/ + try { db = require('./db'); console.log('db успешно импортирован'); @@ -1379,6 +1379,482 @@ function generateTransactions() { return transactions; } + + + +// Начало кода для квестов + +// Маршрут для получения информации о квестах игрока +app.get('/api/quests/player-status', authenticate, async (req, res) => { + try { + const userId = req.user.id; + console.log(`[QUESTS] Запрос статуса квестов для пользователя ${userId}`); + + // Получаем уровень игрока (по умолчанию 1) + const playerLevel = 1; // Пока используем фиксированный уровень + + // Получаем все активные квесты с их статусами для игрока + const questsData = await getPlayerQuestsData(userId, playerLevel); + + console.log(`[QUESTS] Возвращаем данные для пользователя ${userId}: ${questsData.length} квестов`); + res.json({ + success: true, + playerLevel: playerLevel, + quests: questsData + }); + + } catch (error) { + console.error('Ошибка получения статуса квестов игрока:', error); + res.status(500).json({ + success: false, + error: 'Ошибка получения данных о квестах' + }); + } +}); + +// Вспомогательная функция для получения уровня игрока +async function getPlayerLevel(userId) { + try { + // Пока используем уровень по умолчанию 1 + // В будущем можно добавить логику расчета уровня на основе опыта + return 1; + } catch (error) { + console.error('Ошибка получения уровня игрока:', error); + return 1; + } +} + +// Основная функция для получения данных о квестах +async function getPlayerQuestsData(userId, playerLevel) { + try { + // Получаем все доступные квесты + const availableQuests = await new_quest_Base.query(` + SELECT q.id, q.title, q.description, q.kind, q.is_active, q.metadata, + pq.status as player_status, pq.current_step_id, + pq.started_at, pq.completed_at + FROM quests q + LEFT JOIN player_quests pq ON q.id = pq.quest_id AND pq.player_id = $1 + WHERE q.is_active = true + ORDER BY q.id + `, [userId]); + + const questsData = []; + + for (const quest of availableQuests.rows) { + // Проверяем условия доступа к квесту + const hasAccess = await checkQuestAccess(quest.id, playerLevel, userId); + + // Получаем информацию о текущем шаге + const currentStepInfo = await getCurrentStepInfo(quest.id, quest.current_step_id, userId); + + // Получаем все шаги квеста + const questSteps = await new_quest_Base.query(` + SELECT qs.id, qs.step_index, qs.title, qs.description, + qs.action_type, qs.action_payload, qs.dialogue_scene, + qs.is_optional, ps.status as player_step_status, + ps.started_at as step_started, ps.completed_at as step_completed + FROM quest_steps qs + LEFT JOIN player_steps ps ON qs.id = ps.quest_step_id AND ps.player_id = $1 + WHERE qs.quest_id = $2 + ORDER BY qs.step_index + `, [userId, quest.id]); + + // Определяем статус квеста для игрока + let questStatus = 'available'; // доступен + if (quest.player_status === 'completed') { + questStatus = 'completed'; + } else if (quest.player_status === 'in_progress') { + questStatus = 'in_progress'; + } else if (!hasAccess) { + questStatus = 'locked'; + } + + questsData.push({ + id: quest.id, + title: quest.title, + description: quest.description, + kind: quest.kind, + status: questStatus, + hasAccess: hasAccess, + currentStep: currentStepInfo, + steps: questSteps.rows.map(step => ({ + id: step.id, + stepIndex: step.step_index, + title: step.title, + description: step.description, + actionType: step.action_type, + actionPayload: step.action_payload, + dialogueScene: step.dialogue_scene, + isOptional: step.is_optional, + playerStatus: step.player_step_status || 'not_started', + startedAt: step.step_started, + completedAt: step.step_completed + })), + metadata: quest.metadata, + startedAt: quest.started_at, + completedAt: quest.completed_at + }); + } + + return questsData; + + } catch (error) { + console.error('Ошибка получения данных квестов:', error); + throw error; + } +} + +// Функция проверки доступа к квесту +async function checkQuestAccess(questId, playerLevel, userId) { + try { + console.log('Сосал'); + // Проверяем группы условий + const prerequisiteGroups = await new_quest_Base.query(` + SELECT qpg.id, qpg.group_index + FROM quest_prereq_groups qpg + WHERE qpg.quest_id = $1 + ORDER BY qpg.group_index + `, [questId]); + + // Если нет групп условий - квест доступен + if (prerequisiteGroups.rows.length === 0) { + return true; + } + + // Проверяем каждую группу условий + for (const group of prerequisiteGroups.rows) { + const groupConditions = await new_quest_Base.query(` + SELECT qpc.condition_type, qpc.condition_payload + FROM quest_prereq_conditions qpc + WHERE qpc.group_id = $1 + `, [group.id]); + + let allConditionsMet = true; + + for (const condition of groupConditions.rows) { + const conditionMet = await checkCondition( + condition.condition_type, + condition.condition_payload, + playerLevel, + userId + ); + + if (!conditionMet) { + allConditionsMet = false; + break; + } + } + + // Если хотя бы одна группа условий выполнена - квест доступен + if (allConditionsMet) { + return true; + } + } + + return false; + + } catch (error) { + console.error('Ошибка проверки доступа к квесту:', error); + return false; + } +} + +// Функция проверки конкретного условия +async function checkCondition(conditionType, conditionPayload, playerLevel, userId) { + try { + const payload = typeof conditionPayload === 'string' + ? JSON.parse(conditionPayload) + : conditionPayload; + + switch (conditionType) { + case 'level_ge': + return playerLevel >= payload.level; + + case 'quest_completed': + // Проверяем завершенность другого квеста + const questCheck = await new_quest_Base.query(` + SELECT 1 FROM player_quests + WHERE player_id = $1 AND quest_id = $2 AND status = 'completed' + `, [userId, payload.quest_id]); + return questCheck.rows.length > 0; + + case 'step_completed': + // Проверяем завершенность шага + const stepCheck = await new_quest_Base.query(` + SELECT 1 FROM player_steps + WHERE player_id = $1 AND quest_step_id = $2 AND status = 'completed' + `, [userId, payload.step_id]); + return stepCheck.rows.length > 0; + + default: + console.warn(`Неизвестный тип условия: ${conditionType}`); + return false; + } + } catch (error) { + console.error('Ошибка проверки условия:', error); + return false; + } +} + +// Функция получения информации о текущем шаге +async function getCurrentStepInfo(questId, currentStepId, userId) { + if (!currentStepId) { + // Если текущего шага нет, возвращаем первый шаг квеста + const firstStep = await new_quest_Base.query(` + SELECT id, step_index, title, description + FROM quest_steps + WHERE quest_id = $1 + ORDER BY step_index ASC + LIMIT 1 + `, [questId]); + + return firstStep.rows.length > 0 ? { + id: firstStep.rows[0].id, + stepIndex: firstStep.rows[0].step_index, + title: firstStep.rows[0].title, + description: firstStep.rows[0].description + } : null; + } + + // Получаем информацию о текущем шаге + const currentStep = await new_quest_Base.query(` + SELECT qs.id, qs.step_index, qs.title, qs.description, + ps.status as player_status + FROM quest_steps qs + LEFT JOIN player_steps ps ON qs.id = ps.quest_step_id AND ps.player_id = $1 + WHERE qs.id = $2 + `, [userId, currentStepId]); + + return currentStep.rows.length > 0 ? { + id: currentStep.rows[0].id, + stepIndex: currentStep.rows[0].step_index, + title: currentStep.rows[0].title, + description: currentStep.rows[0].description, + playerStatus: currentStep.rows[0].player_status + } : null; +} + +// Маршрут для старта квеста +app.post('/api/quests/:questId/start', authenticate, async (req, res) => { + try { + const userId = req.user.id; + const questId = parseInt(req.params.questId); + const playerLevel = await getPlayerLevel(userId); + + // Проверяем доступность квеста + const hasAccess = await checkQuestAccess(questId, playerLevel, userId); + if (!hasAccess) { + return res.status(403).json({ + success: false, + error: 'Квест недоступен' + }); + } + + // Проверяем, не начат ли уже квест + const existingQuest = await new_quest_Base.query(` + SELECT id FROM player_quests + WHERE player_id = $1 AND quest_id = $2 + `, [userId, questId]); + + if (existingQuest.rows.length > 0) { + return res.status(400).json({ + success: false, + error: 'Квест уже начат' + }); + } + + // Получаем первый шаг квеста + const firstStep = await new_quest_Base.query(` + SELECT id FROM quest_steps + WHERE quest_id = $1 + ORDER BY step_index ASC + LIMIT 1 + `, [questId]); + + if (firstStep.rows.length === 0) { + return res.status(400).json({ + success: false, + error: 'Квест не имеет шагов' + }); + } + + // Начинаем квест + await new_quest_Base.query(` + INSERT INTO player_quests + (player_id, quest_id, current_step_id, status, started_at, last_updated_at) + VALUES ($1, $2, $3, 'in_progress', NOW(), NOW()) + `, [userId, questId, firstStep.rows[0].id]); + + // Записываем первый шаг + await new_quest_Base.query(` + INSERT INTO player_steps + (player_id, quest_step_id, status, started_at) + VALUES ($1, $2, 'in_progress', NOW()) + `, [userId, firstStep.rows[0].id]); + + res.json({ + success: true, + message: 'Квест начат', + currentStepId: firstStep.rows[0].id + }); + + } catch (error) { + console.error('Ошибка старта квеста:', error); + res.status(500).json({ + success: false, + error: 'Ошибка начала квеста' + }); + } +}); + + + +// Маршрут для отметки прослушанного диалога +app.post('/api/quests/mark-dialog-listened', authenticate, async (req, res) => { + try { + const userId = req.user.id; + const { npc_id, dialogue_key } = req.body; + + console.log(`[QUESTS] Отметка прослушанного диалога: user=${userId}, npc=${npc_id}, dialogue=${dialogue_key}`); + + if (!npc_id || !dialogue_key) { + return res.status(400).json({ + success: false, + error: 'Не указаны npc_id или dialogue_key' + }); + } + + // 1. Записываем взаимодействие в player_npc_interactions + const interactionResult = await new_quest_Base.query(` + INSERT INTO player_npc_interactions + (player_id, npc_id, action_type, action_payload, created_at) + VALUES ($1, $2, 'listen_npc', $3, NOW()) + RETURNING id + `, [userId, npc_id, JSON.stringify({ dialogue_key: dialogue_key })]); + + console.log(`[QUESTS] Взаимодействие записано с ID: ${interactionResult.rows[0].id}`); + + // 2. Проверяем, относится ли это взаимодействие к активному шагу квеста + const activeQuestStep = await new_quest_Base.query(` + SELECT pq.id as player_quest_id, pq.current_step_id, qs.id as step_id, qs.action_payload + FROM player_quests pq + JOIN quest_steps qs ON pq.current_step_id = qs.id + WHERE pq.player_id = $1 AND pq.status = 'in_progress' + AND qs.action_type = 'talk_npc' + `, [userId]); + + if (activeQuestStep.rows.length > 0) { + const step = activeQuestStep.rows[0]; + const actionPayload = typeof step.action_payload === 'string' + ? JSON.parse(step.action_payload) + : step.action_payload; + + // Проверяем, соответствует ли NPC текущему шагу квеста + if (actionPayload.npc_id == npc_id) { + console.log(`[QUESTS] Диалог соответствует активному шагу квеста: step_id=${step.step_id}`); + + // 3. Проверяем требования для завершения шага + const stepRequirements = await new_quest_Base.query(` + SELECT requirement_type, requirement_payload + FROM step_requirements + WHERE quest_step_id = $1 + ORDER BY ord + `, [step.step_id]); + + let stepCompleted = true; + + for (const requirement of stepRequirements.rows) { + const payload = typeof requirement.requirement_payload === 'string' + ? JSON.parse(requirement.requirement_payload) + : requirement.requirement_payload; + + if (requirement.requirement_type === 'listen_npc') { + // Проверяем, прослушал ли игрок нужное количество диалогов с указанными NPC + const listenCount = await new_quest_Base.query(` + SELECT COUNT(*) as count + FROM player_npc_interactions + WHERE player_id = $1 + AND npc_id = ANY($2::int[]) + AND action_type = 'listen_npc' + `, [userId, payload.npc_ids]); + + if (listenCount.rows[0].count < payload.count) { + stepCompleted = false; + break; + } + } + } + + if (stepCompleted) { + // 4. Отмечаем шаг как завершенный + await new_quest_Base.query(` + UPDATE player_steps + SET status = 'completed', completed_at = NOW() + WHERE player_id = $1 AND quest_step_id = $2 + `, [userId, step.step_id]); + + // 5. Находим следующий шаг + const nextStep = await new_quest_Base.query(` + SELECT qs.id, qs.step_index + FROM quest_steps qs + WHERE qs.quest_id = ( + SELECT quest_id FROM quest_steps WHERE id = $1 + ) + AND qs.step_index > ( + SELECT step_index FROM quest_steps WHERE id = $1 + ) + ORDER BY qs.step_index ASC + LIMIT 1 + `, [step.step_id]); + + if (nextStep.rows.length > 0) { + // 6. Обновляем текущий шаг в квесте + await new_quest_Base.query(` + UPDATE player_quests + SET current_step_id = $1, last_updated_at = NOW() + WHERE player_id = $2 AND id = $3 + `, [nextStep.rows[0].id, userId, step.player_quest_id]); + + // 7. Начинаем следующий шаг + await new_quest_Base.query(` + INSERT INTO player_steps + (player_id, quest_step_id, status, started_at) + VALUES ($1, $2, 'in_progress', NOW()) + ON CONFLICT (player_id, quest_step_id) + DO UPDATE SET status = 'in_progress', started_at = NOW() + `, [userId, nextStep.rows[0].id]); + + console.log(`[QUESTS] Шаг завершен, переход к шагу ${nextStep.rows[0].step_index}`); + } else { + // 8. Если это последний шаг - завершаем квест + await new_quest_Base.query(` + UPDATE player_quests + SET status = 'completed', completed_at = NOW(), last_updated_at = NOW() + WHERE player_id = $1 AND id = $2 + `, [userId, step.player_quest_id]); + + console.log(`[QUESTS] Квест завершен!`); + } + } + } + } + + res.json({ + success: true, + message: 'Диалог отмечен как прослушанный' + }); + + } catch (error) { + console.error('Ошибка при отметке прослушанного диалога:', error); + res.status(500).json({ + success: false, + error: 'Ошибка сервера при обработке диалога' + }); + } +}); + + + //Конец копи //Начало копи app.get('/api/quests/progress', authenticate, async (req, res) => { diff --git a/src/Game.js b/src/Game.js index 3963da9..f675bdc 100644 --- a/src/Game.js +++ b/src/Game.js @@ -16,9 +16,10 @@ import OrgControlPanel from './components/OrgControlPanel'; import DoubleTapWrapper from './pages/DoubleTapWrapper'; import WaveformPlayer from './pages/WaveformPlayer'; import { getUsersStatus, loadUserInfo } from './api/auth.js'; +import QuestSystem from './pages/QuestSystem '; function Game({ avatarUrl, gender }) { - + const [showQuests, setShowQuests] = useState(false); // 1) реф для хранилища сцены const sceneRef = useRef(new THREE.Scene()); @@ -219,9 +220,9 @@ function Game({ avatarUrl, gender }) { setTgLoading(true); loadTelegramContacts().finally(() => setTgLoading(false)); } - if (appName === "Chrome") { - loadQuestsProgress(); - } + //if (appName === "Chrome") { + // loadQuestsProgress(); + //} if (appName === "Settings") { setShowMiniGame(true); } @@ -6055,7 +6056,25 @@ function Game({ avatarUrl, gender }) { ))} - +
Ошибка загрузки квестов: {error}
+ +{quest.description}
+ + {quest.currentStep && quest.status === 'in_progress' && ( +