Добавлена система квестов
This commit is contained in:
48
db2.js
Normal file
48
db2.js
Normal file
@@ -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()
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
480
server.js
480
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) => {
|
||||
|
||||
33
src/Game.js
33
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 }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 250, // Измените позицию по необходимости
|
||||
zIndex: 1000,
|
||||
padding: '10px 18px',
|
||||
background: '#8B4513',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '18px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
|
||||
}}
|
||||
onClick={() => setShowQuests(true)}
|
||||
>
|
||||
Квесты
|
||||
</button>
|
||||
<div style={{ position: 'absolute', top: 80, left: 20, zIndex: 1000, background: 'rgba(0,0,0,0.6)', color: '#fff', padding: '4px 8px', borderRadius: 4 }}>
|
||||
Баланс: {balance}
|
||||
</div>
|
||||
@@ -6235,7 +6254,9 @@ function Game({ avatarUrl, gender }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showQuests && (
|
||||
<QuestSystem onClose={() => setShowQuests(false)} />
|
||||
)}
|
||||
{selectedHouse && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -8,32 +8,29 @@ export const useDialogManager = () => {
|
||||
const [currentForm, setCurrentForm] = useState(null);
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
const markDialogAsListened = async (jsonFilename) => {
|
||||
const markDialogAsListened = async (npcId, dialogueKey) => {
|
||||
try {
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
const filename = jsonFilename.split('/').pop().split('\\').pop();
|
||||
console.log('Normalized filename:', filename);
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD>111<31>");
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/listen', {
|
||||
const response = await fetch('/api/quests/mark-dialog-listened', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// player_id больше не обязателен: сервер возьмёт его из токена/сессии при наличии
|
||||
json_filename: filename
|
||||
npc_id: npcId,
|
||||
dialogue_key: dialogueKey
|
||||
})
|
||||
});
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>3455654");
|
||||
|
||||
if (!response.ok) {
|
||||
const txt = await response.text().catch(()=> '');
|
||||
console.error('Ошибка при записи прослушанного:', response.status, txt);
|
||||
const txt = await response.text().catch(() => '');
|
||||
console.error('Ошибка при записи прослушанного диалога:', response.status, txt);
|
||||
} else {
|
||||
console.log('Диалог успешно отмечен как прослушанный');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сети при записи прослушанного:', error);
|
||||
console.error('Ошибка сети при записи прослушанного диалога:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,21 +41,32 @@ export const useDialogManager = () => {
|
||||
setCurrentDialog(data);
|
||||
setDialogIndex(0);
|
||||
setShowDialog(true);
|
||||
|
||||
// Получаем dialogue_key из JSON или используем npcId как fallback
|
||||
const dialogueKey = data.dialogue_key || npcId;
|
||||
|
||||
// Записываем начало прослушивания диалога
|
||||
await markDialogAsListened(npcId, dialogueKey);
|
||||
} catch (error) {
|
||||
console.error('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', error);
|
||||
console.error('Ошибка загрузки диалога:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnswerSelect = async (answer) => {
|
||||
console.log('[Debug] Answer object:', answer); // <- <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?
|
||||
console.log('[Debug] "end" in answer:', 'end' in answer); // <- <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> end?
|
||||
console.log('[Debug] Answer object:', answer);
|
||||
|
||||
if (answer.end !== undefined) {
|
||||
console.log('[Debug] Dialog end triggered!');
|
||||
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
if (currentDialog?.filename) {
|
||||
await markDialogAsListened(currentDialog.filename);
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>");
|
||||
|
||||
// При завершении диалога записываем финальное взаимодействие
|
||||
if (currentDialog) {
|
||||
const npcId = currentDialog.npc_id;
|
||||
const dialogueKey = currentDialog.dialogue_key || currentDialog.filename?.replace('.json', '');
|
||||
if (npcId && dialogueKey) {
|
||||
await markDialogAsListened(npcId, dialogueKey);
|
||||
}
|
||||
}
|
||||
|
||||
setShowDialog(false);
|
||||
} else if (answer.next !== undefined) {
|
||||
if (typeof answer.next === 'string' && answer.next.startsWith('form_')) {
|
||||
|
||||
454
src/pages/QuestSystem .jsx
Normal file
454
src/pages/QuestSystem .jsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const QuestSystem = ({ onClose }) => {
|
||||
const [quests, setQuests] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedQuest, setSelectedQuest] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('available');
|
||||
|
||||
// Загрузка квестов при монтировании компонента
|
||||
useEffect(() => {
|
||||
loadQuests();
|
||||
}, []);
|
||||
|
||||
// Функция загрузки квестов с сервера
|
||||
const loadQuests = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const response = await fetch('/api/quests/player-status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка загрузки: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setQuests(data.quests || []);
|
||||
} else {
|
||||
throw new Error(data.error || 'Неизвестная ошибка сервера');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Ошибка загрузки квестов:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция начала квеста
|
||||
const startQuest = async (questId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const response = await fetch(`/api/quests/${questId}/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка начала квеста: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await loadQuests();
|
||||
} else {
|
||||
throw new Error(data.error || 'Неизвестная ошибка сервера');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Ошибка начала квеста:', err);
|
||||
alert(`Ошибка начала квеста: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Фильтрация квестов по статусу
|
||||
const filteredQuests = quests.filter(quest => {
|
||||
switch (activeTab) {
|
||||
case 'available':
|
||||
return quest.status === 'available' && quest.hasAccess;
|
||||
case 'in_progress':
|
||||
return quest.status === 'in_progress';
|
||||
case 'completed':
|
||||
return quest.status === 'completed';
|
||||
case 'locked':
|
||||
return quest.status === 'locked' || !quest.hasAccess;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Стили компонента
|
||||
const styles = {
|
||||
container: {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '80%',
|
||||
maxWidth: '800px',
|
||||
maxHeight: '80vh',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.95)',
|
||||
border: '2px solid #444',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
zIndex: 10000,
|
||||
color: 'white',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
borderBottom: '1px solid #444',
|
||||
paddingBottom: '10px'
|
||||
},
|
||||
closeButton: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 10px'
|
||||
},
|
||||
tabs: {
|
||||
display: 'flex',
|
||||
marginBottom: '20px',
|
||||
borderBottom: '1px solid #444'
|
||||
},
|
||||
tab: {
|
||||
padding: '10px 20px',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: '#aaa',
|
||||
borderBottom: '2px solid transparent',
|
||||
transition: 'all 0.3s'
|
||||
},
|
||||
activeTab: {
|
||||
color: 'white',
|
||||
borderBottom: '2px solid #4CAF50'
|
||||
},
|
||||
questList: {
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
paddingRight: '10px'
|
||||
},
|
||||
questItem: {
|
||||
background: 'rgba(50, 50, 50, 0.7)',
|
||||
borderRadius: '8px',
|
||||
padding: '15px',
|
||||
marginBottom: '10px',
|
||||
border: '1px solid #444',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
},
|
||||
questHeader: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '10px'
|
||||
},
|
||||
questTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
margin: 0
|
||||
},
|
||||
questStatus: {
|
||||
padding: '3px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
statusAvailable: {
|
||||
backgroundColor: '#4CAF50',
|
||||
color: 'white'
|
||||
},
|
||||
statusInProgress: {
|
||||
backgroundColor: '#2196F3',
|
||||
color: 'white'
|
||||
},
|
||||
statusCompleted: {
|
||||
backgroundColor: '#9C27B0',
|
||||
color: 'white'
|
||||
},
|
||||
statusLocked: {
|
||||
backgroundColor: '#757575',
|
||||
color: 'white'
|
||||
},
|
||||
questDescription: {
|
||||
margin: '10px 0',
|
||||
color: '#ccc',
|
||||
fontSize: '14px'
|
||||
},
|
||||
questActions: {
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
marginTop: '10px'
|
||||
},
|
||||
button: {
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
transition: 'all 0.3s'
|
||||
},
|
||||
startButton: {
|
||||
backgroundColor: '#4CAF50',
|
||||
color: 'white'
|
||||
},
|
||||
viewButton: {
|
||||
backgroundColor: '#2196F3',
|
||||
color: 'white'
|
||||
},
|
||||
disabledButton: {
|
||||
backgroundColor: '#757575',
|
||||
color: '#aaa',
|
||||
cursor: 'not-allowed'
|
||||
},
|
||||
questDetail: {
|
||||
marginTop: '20px',
|
||||
padding: '15px',
|
||||
background: 'rgba(40, 40, 40, 0.8)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #444'
|
||||
},
|
||||
stepList: {
|
||||
marginTop: '15px'
|
||||
},
|
||||
stepItem: {
|
||||
padding: '10px',
|
||||
marginBottom: '5px',
|
||||
background: 'rgba(60, 60, 60, 0.6)',
|
||||
borderRadius: '4px',
|
||||
borderLeft: '3px solid #444'
|
||||
},
|
||||
currentStep: {
|
||||
borderLeft: '3px solid #4CAF50',
|
||||
background: 'rgba(76, 175, 80, 0.1)'
|
||||
},
|
||||
completedStep: {
|
||||
borderLeft: '3px solid #9C27B0',
|
||||
background: 'rgba(156, 39, 176, 0.1)'
|
||||
},
|
||||
loading: {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: '#aaa'
|
||||
},
|
||||
error: {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: '#f44336'
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для получения стиля статуса
|
||||
const getStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case 'available': return { ...styles.questStatus, ...styles.statusAvailable };
|
||||
case 'in_progress': return { ...styles.questStatus, ...styles.statusInProgress };
|
||||
case 'completed': return { ...styles.questStatus, ...styles.statusCompleted };
|
||||
case 'locked': return { ...styles.questStatus, ...styles.statusLocked };
|
||||
default: return styles.questStatus;
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для получения текста статуса
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'available': return 'Доступен';
|
||||
case 'in_progress': return 'В процессе';
|
||||
case 'completed': return 'Завершен';
|
||||
case 'locked': return 'Заблокирован';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* ЗДЕСЬ КНОПКА ЗАКРЫТИЯ - внутри header */}
|
||||
<div style={styles.header}>
|
||||
<h2 style={{ margin: 0 }}>Система квестов</h2>
|
||||
<button
|
||||
style={styles.closeButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={styles.tabs}>
|
||||
<button
|
||||
style={activeTab === 'available' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
|
||||
onClick={() => setActiveTab('available')}
|
||||
>
|
||||
Доступные
|
||||
</button>
|
||||
<button
|
||||
style={activeTab === 'in_progress' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
|
||||
onClick={() => setActiveTab('in_progress')}
|
||||
>
|
||||
В процессе
|
||||
</button>
|
||||
<button
|
||||
style={activeTab === 'completed' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
|
||||
onClick={() => setActiveTab('completed')}
|
||||
>
|
||||
Завершенные
|
||||
</button>
|
||||
<button
|
||||
style={activeTab === 'locked' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
|
||||
onClick={() => setActiveTab('locked')}
|
||||
>
|
||||
Заблокированные
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={styles.questList}>
|
||||
{loading ? (
|
||||
<div style={styles.loading}>Загрузка квестов...</div>
|
||||
) : error ? (
|
||||
<div style={styles.error}>
|
||||
<p>Ошибка загрузки квестов: {error}</p>
|
||||
<button
|
||||
style={{ ...styles.button, ...styles.viewButton }}
|
||||
onClick={loadQuests}
|
||||
>
|
||||
Попробовать снова
|
||||
</button>
|
||||
</div>
|
||||
) : filteredQuests.length === 0 ? (
|
||||
<div style={styles.loading}>
|
||||
{activeTab === 'available' && 'Нет доступных квестов'}
|
||||
{activeTab === 'in_progress' && 'Нет активных квестов'}
|
||||
{activeTab === 'completed' && 'Нет завершенных квестов'}
|
||||
{activeTab === 'locked' && 'Нет заблокированных квестов'}
|
||||
</div>
|
||||
) : (
|
||||
filteredQuests.map(quest => (
|
||||
<div
|
||||
key={quest.id}
|
||||
style={styles.questItem}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(70, 70, 70, 0.7)';
|
||||
e.currentTarget.style.borderColor = '#666';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = styles.questItem.background;
|
||||
e.currentTarget.style.borderColor = styles.questItem.borderColor;
|
||||
}}
|
||||
onClick={() => setSelectedQuest(selectedQuest?.id === quest.id ? null : quest)}
|
||||
>
|
||||
<div style={styles.questHeader}>
|
||||
<h3 style={styles.questTitle}>{quest.title}</h3>
|
||||
<span style={getStatusStyle(quest.status)}>
|
||||
{getStatusText(quest.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style={styles.questDescription}>{quest.description}</p>
|
||||
|
||||
{quest.currentStep && quest.status === 'in_progress' && (
|
||||
<div style={{ margin: '10px 0', padding: '8px', background: 'rgba(33, 150, 243, 0.1)', borderRadius: '4px' }}>
|
||||
<strong>Текущий шаг:</strong> {quest.currentStep.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={styles.questActions}>
|
||||
{quest.status === 'available' && quest.hasAccess && (
|
||||
<button
|
||||
style={{ ...styles.button, ...styles.startButton }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startQuest(quest.id);
|
||||
}}
|
||||
>
|
||||
Начать квест
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(quest.status === 'in_progress' || quest.status === 'completed') && (
|
||||
<button
|
||||
style={{ ...styles.button, ...styles.viewButton }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedQuest(selectedQuest?.id === quest.id ? null : quest);
|
||||
}}
|
||||
>
|
||||
{selectedQuest?.id === quest.id ? 'Скрыть детали' : 'Показать детали'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{quest.status === 'locked' && (
|
||||
<button
|
||||
style={{ ...styles.button, ...styles.disabledButton }}
|
||||
disabled
|
||||
>
|
||||
Недоступно
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedQuest?.id === quest.id && (
|
||||
<div style={styles.questDetail}>
|
||||
<h4>Шаги квеста:</h4>
|
||||
<div style={styles.stepList}>
|
||||
{quest.steps.map(step => {
|
||||
let stepStyle = styles.stepItem;
|
||||
if (step.id === quest.currentStep?.id) {
|
||||
stepStyle = { ...styles.stepItem, ...styles.currentStep };
|
||||
} else if (step.playerStatus === 'completed') {
|
||||
stepStyle = { ...styles.stepItem, ...styles.completedStep };
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={step.id} style={stepStyle}>
|
||||
<div style={{ fontWeight: 'bold' }}>
|
||||
Шаг {step.stepIndex + 1}: {step.title}
|
||||
{step.isOptional && ' (Опциональный)'}
|
||||
</div>
|
||||
<div style={{ marginTop: '5px', fontSize: '14px' }}>
|
||||
{step.description}
|
||||
</div>
|
||||
{step.playerStatus && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
marginTop: '5px',
|
||||
color: step.playerStatus === 'completed' ? '#4CAF50' : '#2196F3'
|
||||
}}>
|
||||
Статус: {step.playerStatus === 'completed' ? 'Завершен' : 'В процессе'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuestSystem;
|
||||
Reference in New Issue
Block a user