diff --git a/dialogEditor/.gitignore b/dialogEditor/.gitignore
new file mode 100644
index 0000000..1698216
--- /dev/null
+++ b/dialogEditor/.gitignore
@@ -0,0 +1,43 @@
+.DS_STORE
+node_modules
+scripts/flow/*/.flowconfig
+.flowconfig
+*~
+*.pyc
+.grunt
+_SpecRunner.html
+__benchmarks__
+build/
+remote-repo/
+coverage/
+.module-cache
+fixtures/dom/public/react-dom.js
+fixtures/dom/public/react.js
+test/the-files-to-test.generated.js
+*.log*
+chrome-user-data
+*.sublime-project
+*.sublime-workspace
+.idea
+*.iml
+.vscode
+.zed
+*.swp
+*.swo
+/tmp
+/.worktrees
+.claude/*.local.*
+
+packages/react-devtools-core/dist
+packages/react-devtools-extensions/chrome/build
+packages/react-devtools-extensions/chrome/*.crx
+packages/react-devtools-extensions/chrome/*.pem
+packages/react-devtools-extensions/firefox/build
+packages/react-devtools-extensions/firefox/*.xpi
+packages/react-devtools-extensions/firefox/*.pem
+packages/react-devtools-extensions/shared/build
+packages/react-devtools-extensions/.tempUserDataDir
+packages/react-devtools-fusebox/dist
+packages/react-devtools-inline/dist
+packages/react-devtools-shell/dist
+packages/react-devtools-timeline/dist
diff --git a/dialogEditor/dorm_dialogues.json b/dialogEditor/dorm_dialogues.json
new file mode 100644
index 0000000..37f2449
--- /dev/null
+++ b/dialogEditor/dorm_dialogues.json
@@ -0,0 +1,711 @@
+{
+ "dialogues": [
+ {
+ "id": "dialog_start001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Новый день! Я проснулся, позавтракал и готов поехать в универ! Надо проверить телефон, и не забыть взять свою записную книжку.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_phone001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я не буду никуда идти без своего телефона и записной книжки!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_chat_parents001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Отец",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Бекзат, сынок, мы c мамой тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!",
+ "next": "line_2",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Спасибо!",
+ "next": "end_1",
+ "chatBubble": "out"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_chat_news001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Отец",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Жители Бишкека все чаще жалуются на депрессию и апатию. Смотрите свежее видео об этом на нашем канале!",
+ "next": "end_1",
+ "chatBubble": "in"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_chat_aiperi001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.",
+ "next": "line_2",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Скатерть и тарелки вроде бы лежат в студзоне.",
+ "next": "line_3",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "А нож?",
+ "next": "line_4",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Нож, наверное, так и остался в учительской.",
+ "next": "line_5",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "А давай не \"наверное\"?",
+ "next": "line_6",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?",
+ "next": "line_7",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.",
+ "next": "line_8",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Почему ты сама не можешь забрать?",
+ "next": "line_9",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_9",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.",
+ "next": "line_10",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_10",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?",
+ "next": "line_11",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_11",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Так что жду тебя в универе! Не вздумай прогулять!",
+ "next": "end_1",
+ "chatBubble": "in",
+ "questUnlock" : "aiperi_knife",
+ "luaCallback" : "on_aiperi_dialog_over",
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_no_sleep001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я сейчас не хочу спать.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_phone_pickup001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Отлично, вот и мой телефон! Надо проверить новые сообщения.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_bathroom_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Здесь у меня душ и туалет.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_bathroom_alik_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я не буду лезть в ванную комнату к Алику.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_locked_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Дверь закрыта. Кажется, сюда все еще никто не заселился.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_journal_pickup001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Возьму журнал с собой! Там все мои записи.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_taxi001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Прежде чем выходить наружу, я должен заказать такси до универа.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_taxi002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я заказал такси до универа, машина уже ждет!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_taxi004",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я уже заказал такси, машина уже ждет!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_second_floor001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "На втором этаже женское общежитие, мне там делать нечего.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_female_student001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бермет",
+ "portrait": "resources/dialogue/portrait_student_girl.png",
+ "text": "Бекзат отстань!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_female_student002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Алтынай",
+ "portrait": "resources/dialogue/portrait_student_girl.png",
+ "text": "Бекзат ты почему на пары не ходишь?!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_alik001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Привет Бекзат! Давно я не видел тебя на парах!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_alik_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Тук тук!",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Заходи!",
+ "luaCallback" : "on_alik_room_enter",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_alik002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Привет Бекзат!",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Привет Алик! Разговор есть.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "С тобой на курсе училась Бегимай, ты ее помнишь?",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Конечно помню! Я тебе даже больше расскажу.",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "В тот день она принесла свою курсовую, чтобы сдать.",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Но в тот день в учительской происходила генеральная уборка.",
+ "next": "line_7"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "И получилось так, что ее курсовая оказалась в стопке бумаг на выброс.",
+ "next": "line_8"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Курсовая работа пропала, Бегимай получила за нее ноль баллов, и не прошла отбор в Германию.",
+ "next": "line_9"
+ },
+ {
+ "id": "line_9",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Поэтому с горя она выпрыгнула из окна лекционного зала и убилась.",
+ "next": "line_10"
+ },
+ {
+ "id": "line_10",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "А ты откуда все это знаешь?",
+ "next": "line_11"
+ },
+ {
+ "id": "line_11",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Я видел как ее курсовую уносили вместе с другой макулатурой из учительской.",
+ "next": "line_12"
+ },
+ {
+ "id": "line_12",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "И где сейчас ее курсовая работа?",
+ "next": "line_13"
+ },
+ {
+ "id": "line_13",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "За зданием универа есть контейнер с кучей бумажного мусора и макулатурой.",
+ "next": "line_14"
+ },
+ {
+ "id": "line_14",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Скорее всего, курсовая до сих пор лежит где-то там.",
+ "next": "line_15"
+ },
+ {
+ "id": "line_15",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Спасибо Алик! Ты мне очень помог.",
+ "next": "line_16"
+ },
+ {
+ "id": "line_16",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Да без проблем! Обращайся если что.",
+ "objectiveComplete" : "ghost_lore.ghost_lore_alik",
+ "objectiveVisible": "ghost_lore.ghost_lore_alik",
+ "questUnlock": "ghost_coursework",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_alik003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Алик",
+ "portrait": "resources/dialogue/portrait_student_boy.png",
+ "text": "Привет Бекзат! Надеюсь ты нашел то что ищешь.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_video001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ого, пока я залипал в телефоне, уже наступила ночь!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_video002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я не буду сейчас смотреть видеоролики, давай лучше пойдем спать.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_video003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Мне некогда деградировать, мне нужно сегодня 100% быть на лекции!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ }
+ ],
+ "cutscenes": [{
+ "id": "sleep_cutscene001",
+ "background": "resources/test_cutscene001.png",
+ "onFadeInCallback": "on_sleep_cutscene",
+ "durationMs": 5000,
+ "fadeOutMs": 500,
+ "fadeInMs": 500,
+ "endFadeOutMs": 500,
+ "endFadeInMs": 500,
+ "cameraTrack": [
+ {
+ "durationMs": 3000,
+ "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.10, "rotationDeg": 0.0 },
+ "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.00, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 3000,
+ "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.0, "rotationDeg": 0.0 },
+ "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.1, "rotationDeg": 0.0 },
+ "easing": "EaseInOutCubic"
+ }
+ ],
+ "lines": [
+ {
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я завалился спать и уснул.",
+ "durationMs": 3000
+ },
+ {
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "И я проспал аж до обеда.",
+ "durationMs": 3000
+ }
+ ]
+ }
+ ]
+}
diff --git a/dialogEditor/index.html b/dialogEditor/index.html
new file mode 100644
index 0000000..46772da
--- /dev/null
+++ b/dialogEditor/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Dialogue Editor
+
+
+
+
+
+
diff --git a/dialogEditor/package-lock.json b/dialogEditor/package-lock.json
new file mode 100644
index 0000000..a44c2c7
--- /dev/null
+++ b/dialogEditor/package-lock.json
@@ -0,0 +1,2157 @@
+{
+ "name": "dialogue-editor",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dialogue-editor",
+ "version": "1.0.0",
+ "dependencies": {
+ "@dagrejs/dagre": "^1.1.4",
+ "@xyflow/react": "^12.3.6",
+ "immer": "^10.1.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "zustand": "^5.0.3"
+ },
+ "devDependencies": {
+ "@types/dagre": "^0.7.52",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.5"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dagrejs/dagre": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.8.tgz",
+ "integrity": "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==",
+ "license": "MIT",
+ "dependencies": {
+ "@dagrejs/graphlib": "2.2.4"
+ }
+ },
+ "node_modules/@dagrejs/graphlib": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
+ "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">17.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
+ "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
+ "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
+ "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
+ "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
+ "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
+ "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
+ "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
+ "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
+ "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
+ "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
+ "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
+ "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
+ "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
+ "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
+ "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
+ "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
+ "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
+ "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
+ "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
+ "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
+ "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
+ "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
+ "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/dagre": {
+ "version": "0.7.54",
+ "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.54.tgz",
+ "integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.30",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.30.tgz",
+ "integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@xyflow/react": {
+ "version": "12.11.0",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz",
+ "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.77",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=17",
+ "@types/react-dom": ">=17",
+ "react": ">=17",
+ "react-dom": ">=17"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/react/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.77",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz",
+ "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.33",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz",
+ "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.368",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
+ "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
+ "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.61.1",
+ "@rollup/rollup-android-arm64": "4.61.1",
+ "@rollup/rollup-darwin-arm64": "4.61.1",
+ "@rollup/rollup-darwin-x64": "4.61.1",
+ "@rollup/rollup-freebsd-arm64": "4.61.1",
+ "@rollup/rollup-freebsd-x64": "4.61.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.61.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.61.1",
+ "@rollup/rollup-linux-arm64-musl": "4.61.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.61.1",
+ "@rollup/rollup-linux-loong64-musl": "4.61.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.61.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.61.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.61.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.61.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-musl": "4.61.1",
+ "@rollup/rollup-openbsd-x64": "4.61.1",
+ "@rollup/rollup-openharmony-arm64": "4.61.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.61.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.61.1",
+ "@rollup/rollup-win32-x64-gnu": "4.61.1",
+ "@rollup/rollup-win32-x64-msvc": "4.61.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/dialogEditor/package.json b/dialogEditor/package.json
new file mode 100644
index 0000000..90f9692
--- /dev/null
+++ b/dialogEditor/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "dialogue-editor",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@dagrejs/dagre": "^1.1.4",
+ "@xyflow/react": "^12.3.6",
+ "immer": "^10.1.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "zustand": "^5.0.3"
+ },
+ "devDependencies": {
+ "@types/dagre": "^0.7.52",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/dialogEditor/sample_dialogues.json b/dialogEditor/sample_dialogues.json
new file mode 100644
index 0000000..d31ce4d
--- /dev/null
+++ b/dialogEditor/sample_dialogues.json
@@ -0,0 +1,501 @@
+{
+ "dialogues": [
+ {
+ "id": "dialog_student",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Студент",
+ "portrait": "resources/w/avatar_student.png",
+ "text": "В университете завелись призраки, мне страшно ходить на занятия.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Можешь рассказать подробнее?",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Студент",
+ "portrait": "resources/w/avatar_student.png",
+ "text": "Спроси у Мухтара байке, он все знает.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Хорошо.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_mukhtar",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Мухтар байке",
+ "portrait": "resources/w/avatar_unknown.png",
+ "text": "Здравствуй, мы давно тебя ждем! Ты поможешь нам избавиться от призраков?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Где их найти?",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Мухтар байке",
+ "portrait": "resources/w/avatar_unknown.png",
+ "text": "Заходи в здание универа и поднимайся на второй этаж.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Мухтар байке",
+ "portrait": "resources/w/avatar_unknown.png",
+ "text": "Ты их встретишь прямо там.",
+ "next": "line_5"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Хорошо, я скоро вернусь!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_female_student",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Студентка",
+ "portrait": "resources/w/avatar_girl.png",
+ "text": "С этими призраками совсем невозможно ходить на лекции!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "test_line_dialogue",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Ghost",
+ "portrait": "resources/ghost_avatar.png",
+ "text": "Наконец-то ты пришел.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Ты сделан из дыма?",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Ghost",
+ "portrait": "resources/ghost_avatar.png",
+ "text": "Ты думаешь, это смешно?",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Я думаю что ты пахнешь как выхлоп от Камаза.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "ghost_choice_dialogue",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Беспокойный Призрак",
+ "portrait": "resources/w/avatar_ghost.png",
+ "text": "Нечасто я вижу смертных, готовых разговаривать со мной.",
+ "next": "choice_1"
+ },
+ {
+ "id": "choice_1",
+ "type": "Choice",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "",
+ "choices": [
+ {
+ "id": "main_1",
+ "kind": "Main",
+ "text": "Не мешай студентам учиться!",
+ "next": "line_goods"
+ },
+ {
+ "id": "optional_1",
+ "kind": "Optional",
+ "text": "Почему ты появился здесь?",
+ "next": "line_who"
+ }
+ ]
+ },
+ {
+ "id": "line_goods",
+ "type": "Line",
+ "speaker": "Беспокойный Призрак",
+ "portrait": "resources/w/avatar_ghost.png",
+ "text": "Это моя месть студентам за то что они призвали меня.",
+ "next": "end_1"
+ },
+ {
+ "id": "line_who",
+ "type": "Line",
+ "speaker": "Беспокойный Призрак",
+ "portrait": "resources/w/avatar_ghost.png",
+ "text": "Группа студентов совершила ритуал и призвала меня сюда. Пока проклятие не спадет, я всегда буду здесь обитать.",
+ "next": "choice_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "test_condition_dialogue",
+ "start": "set_flag_1",
+ "nodes": [
+ {
+ "id": "set_flag_1",
+ "type": "SetFlag",
+ "effects": [
+ { "flag": "met_ghost", "value": 1 }
+ ],
+ "next": "condition_1"
+ },
+ {
+ "id": "condition_1",
+ "type": "Condition",
+ "conditions": [
+ { "flag": "met_ghost", "op": "Equals", "value": 1 }
+ ],
+ "trueNext": "line_true",
+ "falseNext": "line_false"
+ },
+ {
+ "id": "line_true",
+ "type": "Line",
+ "speaker": "Ghost",
+ "portrait": "resources/ghost_avatar.png",
+ "text": "Now you know who I am.",
+ "next": "end_1"
+ },
+ {
+ "id": "line_false",
+ "type": "Line",
+ "speaker": "Ghost",
+ "portrait": "resources/ghost_avatar.png",
+ "text": "You should not hear this line.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "test_cutscene_dialogue",
+ "start": "cutscene_start",
+ "nodes": [
+ {
+ "id": "cutscene_start",
+ "type": "CutsceneStart",
+ "cutsceneId": "test_cutscene_01",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "test_silent_cutscene_dialogue",
+ "start": "cutscene_start",
+ "nodes": [
+ {
+ "id": "cutscene_start",
+ "type": "CutsceneStart",
+ "cutsceneId": "test_cutscene_silent_01",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "test_cutscene_pan_dialogue",
+ "start": "cutscene_start",
+ "nodes": [
+ {
+ "id": "cutscene_start",
+ "type": "CutsceneStart",
+ "cutsceneId": "test_cutscene_pan_01",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "test_cutscene_pan_dialogue_silent",
+ "start": "cutscene_start",
+ "nodes": [
+ {
+ "id": "cutscene_start",
+ "type": "CutsceneStart",
+ "cutsceneId": "test_cutscene_pan_02",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_aida",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Асель Дженибековна",
+ "portrait": "resources/w/avatar_teacher.png",
+ "text": "Молодой человек, у меня обед! Я принимаю лабораторные работы только после двух!",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Hero",
+ "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
+ "text": "Хорошо, Асель Дженибековна.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ }
+ ],
+ "cutscenes": [
+ {
+ "id": "test_cutscene_01",
+ "background": "resources/first_cutscene.png",
+ "durationMs": 6800,
+ "cameraTrack": [
+ {
+ "durationMs": 2400,
+ "from": { "focusX": 0.50, "focusY": 0.55, "zoom": 1.00, "rotationDeg": 0.0 },
+ "to": { "focusX": 0.63, "focusY": 0.58, "zoom": 1.16, "rotationDeg": -1.0 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 2200,
+ "from": { "focusX": 0.63, "focusY": 0.58, "zoom": 1.16, "rotationDeg": -1.0 },
+ "to": { "focusX": 0.74, "focusY": 0.52, "zoom": 1.30, "rotationDeg": -2.4 },
+ "easing": "EaseInOutCubic"
+ },
+ {
+ "durationMs": 2200,
+ "from": { "focusX": 0.74, "focusY": 0.52, "zoom": 1.30, "rotationDeg": -2.4 },
+ "to": { "focusX": 0.58, "focusY": 0.46, "zoom": 1.10, "rotationDeg": -0.6 },
+ "easing": "EaseOutSine"
+ }
+ ],
+ "lines": [
+ {
+ "speaker": "Narrator",
+ "portrait": "resources/hero.png",
+ "text": "The air in the room turned cold.",
+ "durationMs": 2200
+ },
+ {
+ "speaker": "Ghost",
+ "portrait": "resources/w/avatar_ghost.png",
+ "text": "Some memories never fade.",
+ "durationMs": 2600,
+ "background": "resources/loading.png"
+ }
+ ]
+ },
+ {
+ "id": "test_cutscene_silent_01",
+ "background": "resources/first_cutscene.png",
+ "durationMs": 5200,
+ "cameraTrack": [
+ {
+ "durationMs": 2600,
+ "from": { "focusX": 0.40, "focusY": 0.54, "zoom": 1.00, "rotationDeg": 0.0 },
+ "to": { "focusX": 0.58, "focusY": 0.54, "zoom": 1.22, "rotationDeg": 0.8 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 2600,
+ "from": { "focusX": 0.58, "focusY": 0.54, "zoom": 1.22, "rotationDeg": 0.8 },
+ "to": { "focusX": 0.72, "focusY": 0.48, "zoom": 1.34, "rotationDeg": -0.5 },
+ "easing": "EaseOutCubic"
+ }
+ ],
+ "lines": []
+ },
+ {
+ "id": "test_cutscene_pan_01",
+ "background": "resources/first_cutscene.png",
+ "durationMs": 12000,
+ "cameraTrack": [
+ {
+ "durationMs": 1200,
+ "from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
+ "to": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
+ "easing": "Linear"
+ },
+ {
+ "durationMs": 2500,
+ "from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
+ "to": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 2600,
+ "from": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
+ "to": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 1800,
+ "from": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
+ "to": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
+ "easing": "EaseInCubic"
+ },
+ {
+ "durationMs": 3900,
+ "from": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
+ "to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ }
+ ],
+ "lines": [
+ {
+ "speaker": "Narrator",
+ "portrait": "resources/hero.png",
+ "text": "The memory begins in silence.",
+ "durationMs": 2200
+ },
+ {
+ "speaker": "Narrator",
+ "portrait": "resources/hero.png",
+ "text": "Something is drawing your eyes across the whole scene.",
+ "durationMs": 2800
+ },
+ {
+ "speaker": "Ghost",
+ "portrait": "resources/ghost_avatar.png",
+ "text": "Do not look away.",
+ "durationMs": 2400
+ }
+ ]
+ },
+ {
+ "id": "test_cutscene_pan_02",
+ "background": "resources/first_cutscene.png",
+ "durationMs": 12000,
+ "cameraTrack": [
+ {
+ "durationMs": 1200,
+ "from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
+ "to": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
+ "easing": "Linear"
+ },
+ {
+ "durationMs": 2500,
+ "from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
+ "to": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 2600,
+ "from": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
+ "to": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 1800,
+ "from": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
+ "to": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
+ "easing": "EaseInCubic"
+ },
+ {
+ "durationMs": 3900,
+ "from": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
+ "to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
+ "easing": "EaseInOutSine"
+ }
+ ],
+ "lines": []
+ }
+ ]
+}
diff --git a/dialogEditor/src/App.module.css b/dialogEditor/src/App.module.css
new file mode 100644
index 0000000..a96eabd
--- /dev/null
+++ b/dialogEditor/src/App.module.css
@@ -0,0 +1,5 @@
+.app {
+ display: flex;
+ height: 100%;
+ overflow: hidden;
+}
diff --git a/dialogEditor/src/App.tsx b/dialogEditor/src/App.tsx
new file mode 100644
index 0000000..0e59e7a
--- /dev/null
+++ b/dialogEditor/src/App.tsx
@@ -0,0 +1,19 @@
+import { LeftPanel } from './components/LeftPanel/LeftPanel';
+import { GraphPanel } from './components/GraphPanel/GraphPanel';
+import { RightPanel } from './components/RightPanel/RightPanel';
+import { PlayModeOverlay } from './components/PlayMode/PlayModeOverlay';
+import { useDialogueStore } from './store/dialogueStore';
+import styles from './App.module.css';
+
+export default function App() {
+ const playModeActive = useDialogueStore(s => s.playModeActive);
+
+ return (
+
+
+
+
+ {playModeActive &&
}
+
+ );
+}
diff --git a/dialogEditor/src/components/GraphPanel/GraphPanel.module.css b/dialogEditor/src/components/GraphPanel/GraphPanel.module.css
new file mode 100644
index 0000000..b08fd05
--- /dev/null
+++ b/dialogEditor/src/components/GraphPanel/GraphPanel.module.css
@@ -0,0 +1,26 @@
+.container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background: #1e1e2e;
+}
+
+.flow {
+ flex: 1;
+ overflow: hidden;
+}
+
+.empty {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #1e1e2e;
+}
+
+.emptyMsg {
+ color: #6c7086;
+ font-size: 14px;
+ text-align: center;
+}
diff --git a/dialogEditor/src/components/GraphPanel/GraphPanel.tsx b/dialogEditor/src/components/GraphPanel/GraphPanel.tsx
new file mode 100644
index 0000000..3c489cf
--- /dev/null
+++ b/dialogEditor/src/components/GraphPanel/GraphPanel.tsx
@@ -0,0 +1,185 @@
+import { useCallback, useEffect, useMemo } from 'react';
+import {
+ ReactFlow,
+ Background,
+ Controls,
+ MiniMap,
+ NodeChange,
+ Node,
+ Edge,
+ Connection,
+ MarkerType,
+} from '@xyflow/react';
+import { useDialogueStore } from '../../store/dialogueStore';
+import { nodeTypes } from '../nodes/nodeTypes';
+import { DialogueNode, ChoiceNode } from '../../types/dialogue';
+import { GraphToolbar } from './GraphToolbar';
+import styles from './GraphPanel.module.css';
+
+function buildEdges(nodes: DialogueNode[]): Edge[] {
+ const edges: Edge[] = [];
+ for (const node of nodes) {
+ const base = {
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#6c7086' },
+ style: { stroke: '#6c7086', strokeWidth: 1.5 },
+ animated: false,
+ };
+
+ if (node.type === 'Line' || node.type === 'SetFlag' || node.type === 'CutsceneStart') {
+ if (node.next) {
+ edges.push({ ...base, id: `${node.id}->source`, source: node.id, target: node.next, sourceHandle: 'source', targetHandle: 'target' });
+ }
+ } else if (node.type === 'Choice') {
+ for (const choice of node.choices) {
+ if (choice.next) {
+ edges.push({ ...base, id: `${node.id}->${choice.id}`, source: node.id, target: choice.next, sourceHandle: choice.id, targetHandle: 'target', label: choice.text.slice(0, 20), labelStyle: { fontSize: 10, fill: '#cdd6f4' }, labelBgStyle: { fill: '#181825' } });
+ }
+ }
+ } else if (node.type === 'Condition') {
+ if (node.trueNext) {
+ edges.push({ ...base, id: `${node.id}->true`, source: node.id, target: node.trueNext, sourceHandle: 'true', targetHandle: 'target', label: 'TRUE', labelStyle: { fontSize: 10, fill: '#a6e3a1' }, labelBgStyle: { fill: '#181825' }, style: { ...base.style, stroke: '#a6e3a1' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#a6e3a1' } });
+ }
+ if (node.falseNext) {
+ edges.push({ ...base, id: `${node.id}->false`, source: node.id, target: node.falseNext, sourceHandle: 'false', targetHandle: 'target', label: 'FALSE', labelStyle: { fontSize: 10, fill: '#f38ba8' }, labelBgStyle: { fill: '#181825' }, style: { ...base.style, stroke: '#f38ba8' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#f38ba8' } });
+ }
+ }
+ }
+ return edges;
+}
+
+export function GraphPanel() {
+ const {
+ file,
+ selectedDialogueId,
+ selectedNodeId,
+ positions,
+ selectNode,
+ setNodePosition,
+ applyAutoLayout,
+ updateNode,
+ } = useDialogueStore();
+
+ const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
+ const dialoguePositions = positions[selectedDialogueId ?? ''] ?? {};
+
+ const rfNodes: Node[] = useMemo(() => {
+ if (!dialogue) return [];
+
+ return dialogue.nodes.map(node => ({
+ id: node.id,
+ type: node.type,
+ data: node as unknown as Record,
+ position: dialoguePositions[node.id] ?? { x: 0, y: 0 },
+ selected: node.id === selectedNodeId,
+ }));
+ }, [dialogue, dialoguePositions, selectedNodeId]);
+
+ const rfEdges: Edge[] = useMemo(() => {
+ if (!dialogue) return [];
+ return buildEdges(dialogue.nodes);
+ }, [dialogue]);
+
+ const onNodesChange = useCallback((changes: NodeChange[]) => {
+ if (!selectedDialogueId) return;
+ for (const change of changes) {
+ if (change.type === 'position' && change.position) {
+ setNodePosition(selectedDialogueId, change.id, change.position);
+ }
+ }
+ }, [selectedDialogueId, setNodePosition]);
+
+ const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
+ selectNode(node.id);
+ }, [selectNode]);
+
+ const onPaneClick = useCallback(() => {
+ selectNode(null);
+ }, [selectNode]);
+
+ const onConnect = useCallback((connection: Connection) => {
+ if (!selectedDialogueId || !connection.source || !connection.target) return;
+ const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
+ if (!dialogue) return;
+ const sourceNode = dialogue.nodes.find(n => n.id === connection.source);
+ if (!sourceNode) return;
+
+ const handle = connection.sourceHandle;
+
+ if (sourceNode.type === 'Line' || sourceNode.type === 'SetFlag' || sourceNode.type === 'CutsceneStart') {
+ updateNode(selectedDialogueId, sourceNode.id, { next: connection.target } as Partial);
+ } else if (sourceNode.type === 'Condition') {
+ if (handle === 'true') {
+ updateNode(selectedDialogueId, sourceNode.id, { trueNext: connection.target } as Partial);
+ } else if (handle === 'false') {
+ updateNode(selectedDialogueId, sourceNode.id, { falseNext: connection.target } as Partial);
+ }
+ } else if (sourceNode.type === 'Choice') {
+ const choices = (sourceNode as ChoiceNode).choices.map(c =>
+ c.id === handle ? { ...c, next: connection.target! } : c
+ );
+ updateNode(selectedDialogueId, sourceNode.id, { choices } as Partial);
+ }
+ }, [selectedDialogueId, file, updateNode]);
+
+ // Trigger auto-layout when dialogue is first loaded with no positions
+ useEffect(() => {
+ if (dialogue && dialogue.nodes.length > 0 && !Object.keys(dialoguePositions).length && selectedDialogueId) {
+ applyAutoLayout(selectedDialogueId);
+ }
+ }, [selectedDialogueId]);
+
+ if (!file) {
+ return (
+
+
Load a dialogue JSON file to get started
+
+ );
+ }
+
+ if (!dialogue) {
+ return (
+
+
Select a dialogue from the left panel
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {
+ switch (n.type) {
+ case 'Line': return '#1e66f5';
+ case 'Choice': return '#df8e1d';
+ case 'Condition': return '#8839ef';
+ case 'SetFlag': return '#179299';
+ case 'CutsceneStart': return '#4a4a5a';
+ case 'End': return '#f38ba8';
+ default: return '#6c7086';
+ }
+ }}
+ style={{ background: '#181825', border: '1px solid #313244' }}
+ />
+
+
+
+ );
+}
diff --git a/dialogEditor/src/components/GraphPanel/GraphToolbar.module.css b/dialogEditor/src/components/GraphPanel/GraphToolbar.module.css
new file mode 100644
index 0000000..3c6c8ef
--- /dev/null
+++ b/dialogEditor/src/components/GraphPanel/GraphToolbar.module.css
@@ -0,0 +1,101 @@
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 10px;
+ background: #181825;
+ border-bottom: 1px solid #313244;
+ flex-wrap: wrap;
+ min-height: 40px;
+}
+
+.group {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.label {
+ font-size: 11px;
+ color: #6c7086;
+ margin-right: 2px;
+}
+
+.btn {
+ background: #313244;
+ color: #cdd6f4;
+ border: none;
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 11px;
+ cursor: pointer;
+ transition: background 0.15s;
+ white-space: nowrap;
+}
+
+.btn:hover {
+ background: #45475a;
+}
+
+.btnPlay {
+ background: #40a02b;
+ color: #fff;
+}
+
+.btnPlay:hover {
+ background: #37872b;
+}
+
+.separator {
+ width: 1px;
+ height: 20px;
+ background: #313244;
+ margin: 0 2px;
+}
+
+.toggle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ font-size: 12px;
+ color: #cdd6f4;
+}
+
+.toggle input {
+ cursor: pointer;
+}
+
+.mobileBanner {
+ background: #2a1f00;
+ color: #f9e2af;
+ font-size: 11px;
+ padding: 3px 10px;
+ border-radius: 4px;
+ border: 1px solid #f9e2af44;
+}
+
+.flagPill {
+ background: rgba(137, 180, 250, 0.15);
+ color: #89b4fa;
+ border-radius: 3px;
+ padding: 2px 6px;
+ font-size: 10px;
+ font-family: monospace;
+ white-space: nowrap;
+}
+
+.btnReset {
+ background: #45475a;
+ color: #f9e2af;
+ border: none;
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 11px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.btnReset:hover {
+ background: #585b70;
+}
diff --git a/dialogEditor/src/components/GraphPanel/GraphToolbar.tsx b/dialogEditor/src/components/GraphPanel/GraphToolbar.tsx
new file mode 100644
index 0000000..01e2100
--- /dev/null
+++ b/dialogEditor/src/components/GraphPanel/GraphToolbar.tsx
@@ -0,0 +1,104 @@
+import { useDialogueStore } from '../../store/dialogueStore';
+import { makeNodeId } from '../../utils/idGen';
+import { DialogueNode, NodeType } from '../../types/dialogue';
+import { MAIN_CHARACTER } from '../../constants/characters';
+import styles from './GraphToolbar.module.css';
+
+const NODE_DEFAULTS: Record Omit> = {
+ Line: () => ({
+ type: 'Line',
+ speaker: MAIN_CHARACTER,
+ portrait: 'resources/dialogue/portrait_hero_neutral.png',
+ text: '',
+ next: '',
+ }),
+ Choice: () => ({
+ type: 'Choice',
+ speaker: MAIN_CHARACTER,
+ portrait: 'resources/dialogue/portrait_hero_neutral.png',
+ text: '',
+ choices: [],
+ }),
+ Condition: () => ({
+ type: 'Condition',
+ conditions: [],
+ trueNext: '',
+ falseNext: '',
+ }),
+ SetFlag: () => ({
+ type: 'SetFlag',
+ effects: [],
+ next: '',
+ }),
+ CutsceneStart: () => ({
+ type: 'CutsceneStart',
+ cutsceneId: '',
+ next: '',
+ }),
+ End: () => ({ type: 'End' }),
+};
+
+export function GraphToolbar() {
+ const { file, selectedDialogueId, selectedNodeId, addNode, applyAutoLayout, startPlay, setDialogueMobileMode, persistentFlags, resetFlags } = useDialogueStore();
+
+ const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
+ if (!dialogue) return null;
+
+ function handleAddNode(type: NodeType) {
+ if (!selectedDialogueId || !dialogue) return;
+ const existingIds = new Set(dialogue.nodes.map(n => n.id));
+ const id = makeNodeId(type, existingIds);
+ const node = { id, ...NODE_DEFAULTS[type]() } as DialogueNode;
+ addNode(selectedDialogueId, node, selectedNodeId ?? undefined);
+ }
+
+ return (
+
+
+ Add:
+ {(['Line', 'Choice', 'Condition', 'SetFlag', 'CutsceneStart', 'End'] as NodeType[]).map(type => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {dialogue.mobileMode && (
+
+ Mobile mode — portraits will be overridden on export
+
+ )}
+ {Object.keys(persistentFlags).length > 0 && (
+ <>
+
+
+ 🚩 Flags:
+ {Object.entries(persistentFlags).map(([k, v]) => (
+ {k}={v}
+ ))}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/dialogEditor/src/components/LeftPanel/LeftPanel.module.css b/dialogEditor/src/components/LeftPanel/LeftPanel.module.css
new file mode 100644
index 0000000..d0f8606
--- /dev/null
+++ b/dialogEditor/src/components/LeftPanel/LeftPanel.module.css
@@ -0,0 +1,185 @@
+.panel {
+ width: 220px;
+ min-width: 220px;
+ background: #181825;
+ border-right: 1px solid #313244;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+
+.header {
+ padding: 12px 12px 6px;
+ border-bottom: 1px solid #313244;
+}
+
+.title {
+ font-size: 13px;
+ font-weight: 700;
+ color: #cdd6f4;
+ display: block;
+}
+
+.fileName {
+ font-size: 10px;
+ color: #6c7086;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.fileButtons {
+ display: flex;
+ gap: 6px;
+ padding: 8px 10px;
+ border-bottom: 1px solid #313244;
+}
+
+.btn {
+ flex: 1;
+ background: #313244;
+ color: #cdd6f4;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.btn:hover {
+ background: #45475a;
+}
+
+.btnSave {
+ background: #1e66f5;
+ color: #fff;
+}
+
+.btnSave:hover {
+ background: #2779e4;
+}
+
+.btnNew {
+ background: #40a02b;
+ color: #fff;
+}
+
+.btnNew:hover {
+ background: #37872b;
+}
+
+.errorMsg {
+ background: #2a0e14;
+ color: #f38ba8;
+ font-size: 11px;
+ padding: 6px 10px;
+ border-top: 1px solid #f38ba8;
+}
+
+.list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 4px 0;
+}
+
+.emptyMsg {
+ padding: 16px 12px;
+ color: #6c7086;
+ font-size: 12px;
+ text-align: center;
+}
+
+.dialogueItem {
+ display: flex;
+ align-items: center;
+ padding: 6px 10px;
+ cursor: pointer;
+ border-radius: 4px;
+ margin: 1px 4px;
+ transition: background 0.1s;
+}
+
+.dialogueItem:hover {
+ background: #313244;
+}
+
+.dialogueItem.active {
+ background: #1e3a5f;
+}
+
+.dialogueId {
+ flex: 1;
+ font-size: 11px;
+ font-family: monospace;
+ color: #cdd6f4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.errDot { color: #f38ba8; font-size: 9px; }
+.warnDot { color: #f9e2af; font-size: 9px; }
+.mobileDot { font-size: 10px; }
+
+.copyBtn {
+ background: none;
+ border: none;
+ color: #6c7086;
+ cursor: pointer;
+ font-size: 13px;
+ line-height: 1;
+ padding: 0 2px;
+ flex-shrink: 0;
+}
+
+.copyBtn:hover {
+ color: #89b4fa;
+}
+
+.deleteBtn {
+ background: none;
+ border: none;
+ color: #6c7086;
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ padding: 0 2px;
+ flex-shrink: 0;
+}
+
+.deleteBtn:hover {
+ color: #f38ba8;
+}
+
+.footer {
+ padding: 8px;
+ border-top: 1px solid #313244;
+}
+
+.createForm {
+ display: flex;
+ gap: 4px;
+}
+
+.createInput {
+ flex: 1;
+ background: #313244;
+ border: 1px solid #45475a;
+ border-radius: 4px;
+ color: #cdd6f4;
+ font-size: 11px;
+ padding: 4px 6px;
+ min-width: 0;
+ font-family: monospace;
+}
+
+.createInput:focus {
+ outline: none;
+ border-color: #89b4fa;
+}
diff --git a/dialogEditor/src/components/LeftPanel/LeftPanel.tsx b/dialogEditor/src/components/LeftPanel/LeftPanel.tsx
new file mode 100644
index 0000000..f6dd0ca
--- /dev/null
+++ b/dialogEditor/src/components/LeftPanel/LeftPanel.tsx
@@ -0,0 +1,156 @@
+import { useRef, useState } from 'react';
+import { useDialogueStore } from '../../store/dialogueStore';
+import { parseDialogueFile } from '../../utils/fileIO';
+import { useValidation } from '../../hooks/useValidation';
+import styles from './LeftPanel.module.css';
+
+export function LeftPanel() {
+ const { file, fileName, selectedDialogueId, loadFile, selectDialogue, createDialogue, deleteDialogue, duplicateDialogue, exportFile } = useDialogueStore();
+ const { issuesByNodeId } = useValidation();
+ const fileInputRef = useRef(null);
+ const [newId, setNewId] = useState('');
+ const [creating, setCreating] = useState(false);
+ const [error, setError] = useState('');
+
+ function handleFileChange(e: React.ChangeEvent) {
+ const f = e.target.files?.[0];
+ if (!f) return;
+ const reader = new FileReader();
+ reader.onload = (ev) => {
+ try {
+ const parsed = parseDialogueFile(ev.target?.result as string);
+ loadFile(parsed, f.name);
+ setError('');
+ } catch (err) {
+ setError(String(err));
+ }
+ };
+ reader.readAsText(f);
+ e.target.value = '';
+ }
+
+ function handleCreate() {
+ if (!newId.trim()) return;
+ if (file?.dialogues.some(d => d.id === newId.trim())) {
+ setError(`ID "${newId.trim()}" already exists`);
+ return;
+ }
+ createDialogue(newId.trim());
+ setNewId('');
+ setCreating(false);
+ setError('');
+ }
+
+ function dialogueHasIssues(dialogueId: string) {
+ // Simple check: are there any issues for nodes in this dialogue?
+ // We only have current dialogue issues so we check if this is selected
+ return selectedDialogueId === dialogueId && Object.keys(issuesByNodeId).length > 0;
+ }
+
+ function dialogueHasErrors(dialogueId: string) {
+ return selectedDialogueId === dialogueId &&
+ Object.values(issuesByNodeId).some(list => list.some(i => i.severity === 'error'));
+ }
+
+ return (
+
+
+ Dialogues
+ {file && {fileName}}
+
+
+
+
+
+ {file && (
+
+ )}
+
+
+ {error &&
{error}
}
+
+
+ {!file && (
+
Load a JSON file to start
+ )}
+ {file?.dialogues.map(d => {
+ const hasErr = dialogueHasErrors(d.id);
+ const hasWarn = !hasErr && dialogueHasIssues(d.id);
+ return (
+
selectDialogue(d.id)}
+ >
+
+ {hasErr && ●}
+ {hasWarn && ●}
+ {d.mobileMode && 📱}
+ {d.id}
+
+
+
+
+ );
+ })}
+
+
+
+ {creating ? (
+
+ setNewId(e.target.value)}
+ placeholder="dialogue_id"
+ onKeyDown={e => {
+ if (e.key === 'Enter') handleCreate();
+ if (e.key === 'Escape') { setCreating(false); setNewId(''); }
+ }}
+ autoFocus
+ />
+
+
+
+ ) : (
+ file && (
+
+ )
+ )}
+
+
+ );
+}
diff --git a/dialogEditor/src/components/PlayMode/PlayModeOverlay.module.css b/dialogEditor/src/components/PlayMode/PlayModeOverlay.module.css
new file mode 100644
index 0000000..50b2d05
--- /dev/null
+++ b/dialogEditor/src/components/PlayMode/PlayModeOverlay.module.css
@@ -0,0 +1,244 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.75);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.dialog {
+ background: #1e1e2e;
+ border: 1px solid #313244;
+ border-radius: 12px;
+ width: 520px;
+ max-width: 90vw;
+ padding: 24px;
+ position: relative;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.7);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.closeBtn {
+ position: absolute;
+ top: 12px;
+ right: 14px;
+ background: none;
+ border: none;
+ color: #6c7086;
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ padding: 4px;
+}
+
+.closeBtn:hover {
+ color: #f38ba8;
+}
+
+.speakerRow {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.portraitBox {
+ width: 52px;
+ height: 52px;
+ border-radius: 8px;
+ background: #313244;
+ overflow: hidden;
+ position: relative;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.portrait {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ position: absolute;
+ inset: 0;
+}
+
+.portraitFallback {
+ font-size: 22px;
+ font-weight: 700;
+ color: #6c7086;
+ text-transform: uppercase;
+}
+
+.speakerName {
+ font-size: 15px;
+ font-weight: 700;
+ color: #89b4fa;
+}
+
+/* Main character (Бекзат) — blue tones */
+.speakerRowMain { border-left: 3px solid #1e66f5; padding-left: 10px; }
+.portraitMain { border: 2px solid #1e66f5; }
+.speakerMain { color: #89b4fa; }
+.textBoxMain { border-left: 3px solid #1e66f5; }
+
+/* Other speakers — green tones */
+.speakerRowOther { border-left: 3px solid #40a02b; padding-left: 10px; }
+.portraitOther { border: 2px solid #40a02b; }
+.speakerOther { color: #a6e3a1; }
+.textBoxOther { border-left: 3px solid #40a02b; }
+
+.textBox {
+ background: #11111b;
+ border-radius: 8px;
+ padding: 14px 16px;
+ font-size: 14px;
+ line-height: 1.6;
+ color: #cdd6f4;
+ min-height: 60px;
+}
+
+.nextBtn {
+ background: #1e66f5;
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 24px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ align-self: flex-end;
+ transition: background 0.15s;
+}
+
+.nextBtn:hover {
+ background: #2779e4;
+}
+
+.noNextWarning {
+ color: #f9e2af;
+ font-size: 12px;
+ text-align: right;
+}
+
+.choiceList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.choiceBtn {
+ border: none;
+ border-radius: 8px;
+ padding: 11px 16px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ text-align: left;
+ transition: opacity 0.15s;
+}
+
+.choiceBtn:hover {
+ opacity: 0.85;
+}
+
+.choiceMain {
+ background: #1e66f5;
+ color: #fff;
+}
+
+.choiceOptional {
+ background: #313244;
+ color: #cdd6f4;
+ border: 1px solid #45475a;
+}
+
+.autoAdvance {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ padding: 16px 0;
+}
+
+.conditionInfo {
+ color: #cba6f7;
+ font-size: 13px;
+}
+
+.conditionClauses {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.clausePill {
+ background: #313244;
+ color: #a6adc8;
+ font-size: 11px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-family: monospace;
+}
+
+.cutsceneBox {
+ background: #11111b;
+ border-radius: 8px;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.cutsceneLabel {
+ font-size: 11px;
+ color: #6c7086;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+}
+
+.cutsceneId {
+ font-size: 15px;
+ color: #a6adc8;
+ font-family: monospace;
+}
+
+.endBox {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 0;
+}
+
+.endMsg {
+ font-size: 16px;
+ color: #6c7086;
+}
+
+.debugBar {
+ border-top: 1px solid #313244;
+ padding-top: 8px;
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.debugLabel {
+ font-size: 10px;
+ color: #45475a;
+ font-family: monospace;
+}
+
+.debugFlagPill {
+ background: rgba(137, 180, 250, 0.12);
+ color: #6c7086;
+ border-radius: 3px;
+ padding: 1px 5px;
+ font-size: 10px;
+ font-family: monospace;
+}
diff --git a/dialogEditor/src/components/PlayMode/PlayModeOverlay.tsx b/dialogEditor/src/components/PlayMode/PlayModeOverlay.tsx
new file mode 100644
index 0000000..a5d971a
--- /dev/null
+++ b/dialogEditor/src/components/PlayMode/PlayModeOverlay.tsx
@@ -0,0 +1,170 @@
+import { useEffect } from 'react';
+import { useDialogueStore } from '../../store/dialogueStore';
+import { ConditionClause } from '../../types/dialogue';
+import { MAIN_CHARACTER } from '../../constants/characters';
+import styles from './PlayModeOverlay.module.css';
+
+function evalConditions(clauses: ConditionClause[], flags: Record): boolean {
+ return clauses.every(c => {
+ const actual = flags[c.flag] ?? 0;
+ switch (c.op) {
+ case 'Equals': return actual == c.value;
+ case 'NotEquals': return actual != c.value;
+ case 'GreaterThan': return Number(actual) > Number(c.value);
+ case 'LessThan': return Number(actual) < Number(c.value);
+ }
+ });
+}
+
+export function PlayModeOverlay() {
+ const { file, selectedDialogueId, playState, advancePlay, setPlayFlag, stopPlay } = useDialogueStore();
+
+ const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
+ const node = dialogue?.nodes.find(n => n.id === playState?.currentNodeId);
+
+ // Auto-advance for Condition and SetFlag
+ useEffect(() => {
+ if (!node || !playState) return;
+
+ if (node.type === 'Condition') {
+ const timer = setTimeout(() => {
+ const passes = evalConditions(node.conditions, playState.flags);
+ advancePlay(passes ? node.trueNext : node.falseNext);
+ }, 300);
+ return () => clearTimeout(timer);
+ }
+
+ if (node.type === 'SetFlag') {
+ const timer = setTimeout(() => {
+ for (const effect of node.effects) {
+ setPlayFlag(effect.flag, effect.value);
+ }
+ advancePlay(node.next);
+ }, 100);
+ return () => clearTimeout(timer);
+ }
+ }, [node?.id, node?.type]);
+
+ if (!playState || !node) return null;
+
+ const portraitPath = node.type === 'Line' || node.type === 'Choice'
+ ? `../${node.portrait}`
+ : null;
+
+ return (
+ e.stopPropagation()}>
+
+
+
+ {node.type === 'Line' && (
+ <>
+
+
+

{ (e.target as HTMLImageElement).style.display = 'none'; }}
+ />
+
{node.speaker[0]}
+
+
{node.speaker}
+
+
{node.text}
+ {node.next ? (
+
+ ) : (
+
⚠ No next node set
+ )}
+ >
+ )}
+
+ {node.type === 'Choice' && (
+ <>
+
+
+

{ (e.target as HTMLImageElement).style.display = 'none'; }}
+ />
+
{node.speaker[0]}
+
+
{node.speaker}
+
+ {node.text &&
{node.text}
}
+
+ {node.choices.map(choice => (
+
+ ))}
+
+ >
+ )}
+
+ {node.type === 'Condition' && (
+
+
Evaluating condition...
+
+ {node.conditions.map((c, i) => (
+ {c.flag} {c.op} {c.value}
+ ))}
+
+
+ )}
+
+ {node.type === 'SetFlag' && (
+
+ )}
+
+ {node.type === 'CutsceneStart' && (
+ <>
+
+ Cutscene:
+ {node.cutsceneId}
+
+
+ >
+ )}
+
+ {node.type === 'End' && (
+
+
Dialogue ended.
+
+
+ )}
+
+
+ node: {node.id}
+ {Object.entries(playState.flags).map(([k, v]) => (
+ {k}={v}
+ ))}
+
+
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/RightPanel.module.css b/dialogEditor/src/components/RightPanel/RightPanel.module.css
new file mode 100644
index 0000000..d748030
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/RightPanel.module.css
@@ -0,0 +1,313 @@
+.panel {
+ width: 300px;
+ min-width: 300px;
+ background: #181825;
+ border-left: 1px solid #313244;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow-y: auto;
+}
+
+.emptyMsg {
+ padding: 24px 16px;
+ color: #6c7086;
+ font-size: 13px;
+ text-align: center;
+}
+
+.inspector {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.inspectorHeader {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+
+.nodeTypeBadge {
+ font-size: 11px;
+ font-weight: 700;
+ padding: 3px 8px;
+ border-radius: 4px;
+ color: #fff;
+}
+
+.nodeIdLabel {
+ flex: 1;
+ font-size: 11px;
+ font-family: monospace;
+ color: #6c7086;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.deleteNodeBtn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ padding: 2px;
+ color: #6c7086;
+ transition: color 0.15s;
+}
+
+.deleteNodeBtn:hover {
+ color: #f38ba8;
+}
+
+.label {
+ display: block;
+ font-size: 10px;
+ font-weight: 600;
+ color: #6c7086;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 3px;
+}
+
+.input {
+ width: 100%;
+ background: #1e1e2e;
+ border: 1px solid #313244;
+ border-radius: 4px;
+ color: #cdd6f4;
+ font-size: 12px;
+ padding: 5px 8px;
+ box-sizing: border-box;
+ font-family: inherit;
+ transition: border-color 0.15s;
+}
+
+.input:focus {
+ outline: none;
+ border-color: #89b4fa;
+}
+
+.textarea {
+ width: 100%;
+ background: #1e1e2e;
+ border: 1px solid #313244;
+ border-radius: 4px;
+ color: #cdd6f4;
+ font-size: 12px;
+ padding: 5px 8px;
+ box-sizing: border-box;
+ resize: vertical;
+ font-family: inherit;
+ line-height: 1.5;
+ transition: border-color 0.15s;
+}
+
+.textarea:focus {
+ outline: none;
+ border-color: #89b4fa;
+}
+
+.select {
+ width: 100%;
+ background: #1e1e2e;
+ border: 1px solid #313244;
+ border-radius: 4px;
+ color: #cdd6f4;
+ font-size: 12px;
+ padding: 5px 8px;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.select:focus {
+ outline: none;
+ border-color: #89b4fa;
+}
+
+.selectSmall {
+ background: #1e1e2e;
+ border: 1px solid #313244;
+ border-radius: 4px;
+ color: #cdd6f4;
+ font-size: 11px;
+ padding: 3px 6px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.inputSmall {
+ flex: 1;
+ min-width: 0;
+ background: #1e1e2e;
+ border: 1px solid #313244;
+ border-radius: 4px;
+ color: #cdd6f4;
+ font-size: 11px;
+ padding: 3px 6px;
+ font-family: monospace;
+}
+
+.inputSmall:focus {
+ outline: none;
+ border-color: #89b4fa;
+}
+
+.advancedToggle {
+ background: none;
+ border: none;
+ color: #6c7086;
+ font-size: 11px;
+ cursor: pointer;
+ padding: 4px 0;
+ text-align: left;
+}
+
+.advancedToggle:hover {
+ color: #cdd6f4;
+}
+
+.advancedSection {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px;
+ background: #1e1e2e;
+ border-radius: 4px;
+ border: 1px solid #313244;
+}
+
+.issueList {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.issueError {
+ background: #2a0e14;
+ color: #f38ba8;
+ font-size: 11px;
+ padding: 5px 8px;
+ border-radius: 4px;
+ border-left: 3px solid #f38ba8;
+}
+
+.issueWarning {
+ background: #2a1f00;
+ color: #f9e2af;
+ font-size: 11px;
+ padding: 5px 8px;
+ border-radius: 4px;
+ border-left: 3px solid #f9e2af;
+}
+
+/* Choice editor */
+.choiceEditorRow {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 8px;
+ background: #1e1e2e;
+ border-radius: 4px;
+ border: 1px solid #313244;
+ margin-top: 4px;
+}
+
+.choiceEditorHeader {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.choiceIdLabel {
+ flex: 1;
+ font-size: 10px;
+ color: #6c7086;
+}
+
+.sectionHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 4px;
+}
+
+.addBtn {
+ background: #313244;
+ color: #cdd6f4;
+ border: none;
+ border-radius: 4px;
+ padding: 3px 8px;
+ font-size: 11px;
+ cursor: pointer;
+}
+
+.addBtn:hover {
+ background: #45475a;
+}
+
+.removeBtn {
+ background: none;
+ border: none;
+ color: #6c7086;
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ padding: 0 2px;
+}
+
+.removeBtn:hover {
+ color: #f38ba8;
+}
+
+/* Clause / effect rows */
+.clauseRow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-top: 4px;
+}
+
+.eqLabel {
+ color: #cba6f7;
+ font-weight: 700;
+ font-size: 13px;
+}
+
+.emptyHint {
+ color: #6c7086;
+ font-size: 11px;
+ font-style: italic;
+ padding: 4px 0;
+}
+
+/* Dialogue meta panel */
+.dialogueMeta {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.metaHeader {
+ font-size: 12px;
+ font-weight: 700;
+ color: #cdd6f4;
+ margin-bottom: 4px;
+}
+
+.statRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.statPill {
+ background: #313244;
+ color: #a6adc8;
+ font-size: 10px;
+ padding: 2px 7px;
+ border-radius: 10px;
+}
diff --git a/dialogEditor/src/components/RightPanel/RightPanel.tsx b/dialogEditor/src/components/RightPanel/RightPanel.tsx
new file mode 100644
index 0000000..b1dab00
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/RightPanel.tsx
@@ -0,0 +1,102 @@
+import { useDialogueStore } from '../../store/dialogueStore';
+import { LineInspector } from './inspectors/LineInspector';
+import { ChoiceInspector } from './inspectors/ChoiceInspector';
+import { ConditionInspector } from './inspectors/ConditionInspector';
+import { SetFlagInspector } from './inspectors/SetFlagInspector';
+import { CutsceneStartInspector, EndInspector } from './inspectors/OtherInspectors';
+import { DialogueNode } from '../../types/dialogue';
+import styles from './RightPanel.module.css';
+
+// Helper to update start node ID
+function useUpdateStart() {
+ const store = useDialogueStore;
+ return (dialogueId: string, start: string) => {
+ store.setState(state => {
+ const d = state.file?.dialogues.find(x => x.id === dialogueId);
+ if (d) d.start = start;
+ });
+ };
+}
+
+export function RightPanel() {
+ const { file, selectedDialogueId, selectedNodeId } = useDialogueStore();
+ const updateStart = useUpdateStart();
+
+ const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
+
+ if (!dialogue) {
+ return (
+
+ );
+ }
+
+ if (!selectedNodeId) {
+ return (
+
+
+
Dialogue Properties
+
+
+
+
+
+
+ {
+ if (selectedDialogueId) updateStart(selectedDialogueId, e.target.value);
+ }}
+ list="startNodeList"
+ />
+
+
+
+ Click a node in the graph to inspect it.
+
+
+
Node Count
+
+ {(['Line', 'Choice', 'Condition', 'SetFlag', 'CutsceneStart', 'End'] as DialogueNode['type'][]).map(t => {
+ const count = dialogue.nodes.filter(n => n.type === t).length;
+ return count > 0 ? (
+ {t}: {count}
+ ) : null;
+ })}
+
+
+
+
+ );
+ }
+
+ const node = dialogue.nodes.find(n => n.id === selectedNodeId);
+ if (!node) return null;
+
+ return (
+
+ {node.type === 'Line' && (
+
+ )}
+ {node.type === 'Choice' && (
+
+ )}
+ {node.type === 'Condition' && (
+
+ )}
+ {node.type === 'SetFlag' && (
+
+ )}
+ {node.type === 'CutsceneStart' && (
+
+ )}
+ {node.type === 'End' && (
+
+ )}
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/inspectors/ChoiceInspector.tsx b/dialogEditor/src/components/RightPanel/inspectors/ChoiceInspector.tsx
new file mode 100644
index 0000000..7a9ab3c
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/inspectors/ChoiceInspector.tsx
@@ -0,0 +1,110 @@
+import { ChoiceNode, ChoiceOption } from '../../../types/dialogue';
+import { useDialogueStore } from '../../../store/dialogueStore';
+import { SpeakerField } from '../shared/SpeakerField';
+import { useValidation } from '../../../hooks/useValidation';
+import styles from '../RightPanel.module.css';
+
+interface Props {
+ node: ChoiceNode;
+ dialogueId: string;
+}
+
+export function ChoiceInspector({ node, dialogueId }: Props) {
+ const { updateNode, deleteNode, file } = useDialogueStore();
+ const { issuesByNodeId } = useValidation();
+ const issues = issuesByNodeId[node.id] ?? [];
+
+ const dialogue = file?.dialogues.find(d => d.id === dialogueId);
+ const nodeIds = dialogue?.nodes.map(n => n.id) ?? [];
+
+ function updateChoice(idx: number, patch: Partial) {
+ const choices = node.choices.map((c, i) => i === idx ? { ...c, ...patch } : c);
+ updateNode(dialogueId, node.id, { choices });
+ }
+
+ function addChoice() {
+ const newId = `choice_${Date.now()}`;
+ const choices: ChoiceOption[] = [...node.choices, { id: newId, kind: 'Optional', text: '', next: '' }];
+ updateNode(dialogueId, node.id, { choices });
+ }
+
+ function removeChoice(idx: number) {
+ const choices = node.choices.filter((_, i) => i !== idx);
+ updateNode(dialogueId, node.id, { choices });
+ }
+
+ return (
+
+
+ Choice
+ {node.id}
+
+
+
+ {issues.length > 0 && (
+
+ {issues.map((issue, i) => (
+
+ {issue.severity === 'error' ? '⚠' : '!'} {issue.message}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
updateNode(dialogueId, node.id, { speaker: s, portrait: p })}
+ />
+
+
+
+ Choices
+
+
+
+ {node.choices.map((choice, idx) => (
+
+
+
+ #{idx + 1}
+
+
+
updateChoice(idx, { text: e.target.value })}
+ />
+
updateChoice(idx, { next: e.target.value })}
+ list={`nodelist-choice-${choice.id}`}
+ />
+
+
+ ))}
+
+ {node.choices.length === 0 && (
+
No choices yet. Click "+ Add".
+ )}
+
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/inspectors/ConditionInspector.tsx b/dialogEditor/src/components/RightPanel/inspectors/ConditionInspector.tsx
new file mode 100644
index 0000000..f16c477
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/inspectors/ConditionInspector.tsx
@@ -0,0 +1,120 @@
+import { ConditionNode, ConditionClause, ConditionOp } from '../../../types/dialogue';
+import { useDialogueStore } from '../../../store/dialogueStore';
+import { useValidation } from '../../../hooks/useValidation';
+import styles from '../RightPanel.module.css';
+
+const OPS: ConditionOp[] = ['Equals', 'NotEquals', 'GreaterThan', 'LessThan'];
+
+interface Props {
+ node: ConditionNode;
+ dialogueId: string;
+}
+
+export function ConditionInspector({ node, dialogueId }: Props) {
+ const { updateNode, deleteNode, file } = useDialogueStore();
+ const { issuesByNodeId } = useValidation();
+ const issues = issuesByNodeId[node.id] ?? [];
+
+ const dialogue = file?.dialogues.find(d => d.id === dialogueId);
+ const nodeIds = dialogue?.nodes.map(n => n.id) ?? [];
+
+ function updateClause(idx: number, patch: Partial) {
+ const conditions = node.conditions.map((c, i) => i === idx ? { ...c, ...patch } : c);
+ updateNode(dialogueId, node.id, { conditions });
+ }
+
+ function addClause() {
+ const conditions: ConditionClause[] = [...node.conditions, { flag: '', op: 'Equals', value: 1 }];
+ updateNode(dialogueId, node.id, { conditions });
+ }
+
+ function removeClause(idx: number) {
+ const conditions = node.conditions.filter((_, i) => i !== idx);
+ updateNode(dialogueId, node.id, { conditions });
+ }
+
+ return (
+
+
+ Condition
+ {node.id}
+
+
+
+ {issues.length > 0 && (
+
+ {issues.map((issue, i) => (
+
+ {issue.severity === 'error' ? '⚠' : '!'} {issue.message}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ Conditions (ALL must pass)
+
+
+ {node.conditions.map((c, idx) => (
+
+ updateClause(idx, { flag: e.target.value })}
+ />
+
+ updateClause(idx, { value: isNaN(Number(e.target.value)) ? e.target.value : Number(e.target.value) })}
+ />
+
+
+ ))}
+ {node.conditions.length === 0 &&
No conditions. Always true.
}
+
+
+
+
+ updateNode(dialogueId, node.id, { trueNext: e.target.value })}
+ list={`nodelist-true-${node.id}`}
+ />
+
+
+
+
+
+ updateNode(dialogueId, node.id, { falseNext: e.target.value })}
+ list={`nodelist-false-${node.id}`}
+ />
+
+
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/inspectors/LineInspector.tsx b/dialogEditor/src/components/RightPanel/inspectors/LineInspector.tsx
new file mode 100644
index 0000000..a6e33b7
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/inspectors/LineInspector.tsx
@@ -0,0 +1,114 @@
+import { useState } from 'react';
+import { LineNode } from '../../../types/dialogue';
+import { useDialogueStore } from '../../../store/dialogueStore';
+import { SpeakerField } from '../shared/SpeakerField';
+import { AutoSaveTextArea } from '../shared/TextArea';
+import { useValidation } from '../../../hooks/useValidation';
+import styles from '../RightPanel.module.css';
+
+interface Props {
+ node: LineNode;
+ dialogueId: string;
+}
+
+export function LineInspector({ node, dialogueId }: Props) {
+ const { updateNode, deleteNode, file } = useDialogueStore();
+ const { issuesByNodeId } = useValidation();
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const issues = issuesByNodeId[node.id] ?? [];
+
+ const dialogue = file?.dialogues.find(d => d.id === dialogueId);
+ const nodeIds = dialogue?.nodes.map(n => n.id) ?? [];
+
+ function update(patch: Partial) {
+ updateNode(dialogueId, node.id, patch);
+ }
+
+ function field(label: string, key: keyof LineNode, placeholder?: string) {
+ const val = (node[key] as string) ?? '';
+ return (
+
+
+ update({ [key]: e.target.value } as Partial)}
+ list={key === 'next' ? `nodelist-${node.id}` : undefined}
+ />
+ {key === 'next' && (
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+ Line
+ {node.id}
+
+
+
+ {issues.length > 0 && (
+
+ {issues.map((issue, i) => (
+
+ {issue.severity === 'error' ? '⚠' : '!'} {issue.message}
+
+ ))}
+
+ )}
+
+ {field('Node ID', 'id')}
+
update({ speaker: s, portrait: p })}
+ />
+ update({ text: v })}
+ />
+ {field('Next node', 'next', 'node_id')}
+
+ {dialogue?.mobileMode && (
+
+
+
+
+ )}
+
+
+
+ {showAdvanced && (
+
+ {field('questUnlock', 'questUnlock', 'quest_id')}
+ {field('objectiveComplete', 'objectiveComplete', 'group.objective')}
+ {field('objectiveVisible', 'objectiveVisible', 'group.objective')}
+ {field('questFail', 'questFail', 'quest_id')}
+ {field('questComplete', 'questComplete', 'quest_id')}
+ {field('luaCallback', 'luaCallback', 'on_event_name')}
+
+ )}
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/inspectors/OtherInspectors.tsx b/dialogEditor/src/components/RightPanel/inspectors/OtherInspectors.tsx
new file mode 100644
index 0000000..ccbba72
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/inspectors/OtherInspectors.tsx
@@ -0,0 +1,76 @@
+import { CutsceneStartNode, EndNode } from '../../../types/dialogue';
+import { useDialogueStore } from '../../../store/dialogueStore';
+import styles from '../RightPanel.module.css';
+
+interface CutsceneProps {
+ node: CutsceneStartNode;
+ dialogueId: string;
+}
+
+export function CutsceneStartInspector({ node, dialogueId }: CutsceneProps) {
+ const { updateNode, deleteNode, file } = useDialogueStore();
+ const dialogue = file?.dialogues.find(d => d.id === dialogueId);
+ const nodeIds = dialogue?.nodes.map(n => n.id) ?? [];
+
+ return (
+
+
+ CutsceneStart
+ {node.id}
+
+
+
+
+
+
+
+
+ updateNode(dialogueId, node.id, { cutsceneId: e.target.value })}
+ />
+
+
+
+ updateNode(dialogueId, node.id, { next: e.target.value })}
+ list={`nodelist-cs-${node.id}`}
+ />
+
+
+
+ );
+}
+
+interface EndProps {
+ node: EndNode;
+ dialogueId: string;
+}
+
+export function EndInspector({ node, dialogueId }: EndProps) {
+ const { deleteNode } = useDialogueStore();
+
+ return (
+
+
+ End
+ {node.id}
+
+
+
+
+
+
+
+ This is a terminal node. Dialogue ends here.
+
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/inspectors/SetFlagInspector.tsx b/dialogEditor/src/components/RightPanel/inspectors/SetFlagInspector.tsx
new file mode 100644
index 0000000..46343c4
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/inspectors/SetFlagInspector.tsx
@@ -0,0 +1,98 @@
+import { SetFlagNode, FlagEffect } from '../../../types/dialogue';
+import { useDialogueStore } from '../../../store/dialogueStore';
+import { useValidation } from '../../../hooks/useValidation';
+import styles from '../RightPanel.module.css';
+
+interface Props {
+ node: SetFlagNode;
+ dialogueId: string;
+}
+
+export function SetFlagInspector({ node, dialogueId }: Props) {
+ const { updateNode, deleteNode, file } = useDialogueStore();
+ const { issuesByNodeId } = useValidation();
+ const issues = issuesByNodeId[node.id] ?? [];
+
+ const dialogue = file?.dialogues.find(d => d.id === dialogueId);
+ const nodeIds = dialogue?.nodes.map(n => n.id) ?? [];
+
+ function updateEffect(idx: number, patch: Partial) {
+ const effects = node.effects.map((e, i) => i === idx ? { ...e, ...patch } : e);
+ updateNode(dialogueId, node.id, { effects });
+ }
+
+ function addEffect() {
+ const effects: FlagEffect[] = [...node.effects, { flag: '', value: 1 }];
+ updateNode(dialogueId, node.id, { effects });
+ }
+
+ function removeEffect(idx: number) {
+ const effects = node.effects.filter((_, i) => i !== idx);
+ updateNode(dialogueId, node.id, { effects });
+ }
+
+ return (
+
+
+ SetFlag
+ {node.id}
+
+
+
+ {issues.length > 0 && (
+
+ {issues.map((issue, i) => (
+
+ {issue.severity === 'error' ? '⚠' : '!'} {issue.message}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ Flag Effects
+
+
+ {node.effects.map((e, idx) => (
+
+ updateEffect(idx, { flag: ev.target.value })}
+ />
+ =
+ updateEffect(idx, { value: isNaN(Number(ev.target.value)) ? ev.target.value : Number(ev.target.value) })}
+ />
+
+
+ ))}
+ {node.effects.length === 0 &&
No effects defined.
}
+
+
+
+
+ updateNode(dialogueId, node.id, { next: e.target.value })}
+ list={`nodelist-sf-${node.id}`}
+ />
+
+
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/shared/SpeakerField.tsx b/dialogEditor/src/components/RightPanel/shared/SpeakerField.tsx
new file mode 100644
index 0000000..b8912e5
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/shared/SpeakerField.tsx
@@ -0,0 +1,58 @@
+import { useState } from 'react';
+import { CHARACTER_PRESETS, CUSTOM_CHARACTER_LABEL } from '../../../constants/characters';
+import styles from '../RightPanel.module.css';
+
+interface Props {
+ speaker: string;
+ portrait: string;
+ onSpeakerChange: (speaker: string, portrait: string) => void;
+}
+
+export function SpeakerField({ speaker, portrait, onSpeakerChange }: Props) {
+ const isPreset = CHARACTER_PRESETS.some(p => p.label === speaker);
+ const [isCustom, setIsCustom] = useState(!isPreset);
+
+ function handleSelect(e: React.ChangeEvent) {
+ const val = e.target.value;
+ if (val === CUSTOM_CHARACTER_LABEL) {
+ setIsCustom(true);
+ onSpeakerChange(speaker, portrait);
+ } else {
+ setIsCustom(false);
+ const preset = CHARACTER_PRESETS.find(p => p.label === val);
+ onSpeakerChange(val, preset?.portrait ?? '');
+ }
+ }
+
+ return (
+
+
+
+ {isCustom && (
+ onSpeakerChange(e.target.value, portrait)}
+ />
+ )}
+
+ onSpeakerChange(speaker, e.target.value)}
+ />
+
+ );
+}
diff --git a/dialogEditor/src/components/RightPanel/shared/TextArea.tsx b/dialogEditor/src/components/RightPanel/shared/TextArea.tsx
new file mode 100644
index 0000000..0e1ae59
--- /dev/null
+++ b/dialogEditor/src/components/RightPanel/shared/TextArea.tsx
@@ -0,0 +1,26 @@
+import { useAutoSave } from '../../../hooks/useAutoSave';
+import styles from '../RightPanel.module.css';
+
+interface Props {
+ value: string;
+ label: string;
+ placeholder?: string;
+ onSave: (value: string) => void;
+ rows?: number;
+}
+
+export function AutoSaveTextArea({ value, label, placeholder, onSave, rows = 4 }: Props) {
+ const [local, handleChange] = useAutoSave(value, onSave);
+ return (
+
+
+
+ );
+}
diff --git a/dialogEditor/src/components/nodes/ChoiceNode.tsx b/dialogEditor/src/components/nodes/ChoiceNode.tsx
new file mode 100644
index 0000000..6ff7222
--- /dev/null
+++ b/dialogEditor/src/components/nodes/ChoiceNode.tsx
@@ -0,0 +1,65 @@
+import { memo } from 'react';
+import { Handle, Position, NodeProps } from '@xyflow/react';
+import { ChoiceNode as ChoiceNodeData } from '../../types/dialogue';
+import { useValidation } from '../../hooks/useValidation';
+import { useDialogueStore } from '../../store/dialogueStore';
+import styles from './nodes.module.css';
+
+export const ChoiceNode = memo(({ data, selected }: NodeProps & { data: ChoiceNodeData }) => {
+ const { hasError, hasWarning } = useValidation();
+ const startId = useDialogueStore(s => {
+ const id = s.selectedDialogueId;
+ return s.file?.dialogues.find(d => d.id === id)?.start;
+ });
+
+ const isStart = data.id === startId;
+ const error = hasError(data.id);
+ const warning = !error && hasWarning(data.id);
+
+ return (
+
+ {isStart &&
▶ START
}
+
+
+ {data.speaker || '(no speaker)'}
+ Choice
+
+
{data.id}
+
+ {data.choices.length === 0 ? (
+
(no choices)
+ ) : (
+ data.choices.map((choice, i) => (
+
+
+ {choice.kind}
+
+ {choice.text || '(empty)'}
+
+
+ ))
+ )}
+
+
+ );
+});
diff --git a/dialogEditor/src/components/nodes/ConditionNode.tsx b/dialogEditor/src/components/nodes/ConditionNode.tsx
new file mode 100644
index 0000000..6b36664
--- /dev/null
+++ b/dialogEditor/src/components/nodes/ConditionNode.tsx
@@ -0,0 +1,60 @@
+import { memo } from 'react';
+import { Handle, Position, NodeProps } from '@xyflow/react';
+import { ConditionNode as ConditionNodeData } from '../../types/dialogue';
+import { useValidation } from '../../hooks/useValidation';
+import styles from './nodes.module.css';
+
+export const ConditionNode = memo(({ data, selected }: NodeProps & { data: ConditionNodeData }) => {
+ const { hasError, hasWarning } = useValidation();
+ const error = hasError(data.id);
+ const warning = !error && hasWarning(data.id);
+
+ return (
+
+
+
+ Condition
+ If
+
+
{data.id}
+
+ {data.conditions.map((c, i) => (
+
+ {c.flag}
+ {c.op}
+ {String(c.value)}
+
+ ))}
+ {data.conditions.length === 0 && (
+
(no conditions)
+ )}
+
+
+ TRUE
+ FALSE
+
+
+
+
+ );
+});
diff --git a/dialogEditor/src/components/nodes/CutsceneStartNode.tsx b/dialogEditor/src/components/nodes/CutsceneStartNode.tsx
new file mode 100644
index 0000000..6427f00
--- /dev/null
+++ b/dialogEditor/src/components/nodes/CutsceneStartNode.tsx
@@ -0,0 +1,34 @@
+import { memo } from 'react';
+import { Handle, Position, NodeProps } from '@xyflow/react';
+import { CutsceneStartNode as CutsceneStartNodeData } from '../../types/dialogue';
+import { useValidation } from '../../hooks/useValidation';
+import styles from './nodes.module.css';
+
+export const CutsceneStartNode = memo(({ data, selected }: NodeProps & { data: CutsceneStartNodeData }) => {
+ const { hasError, hasWarning } = useValidation();
+ const error = hasError(data.id);
+ const warning = !error && hasWarning(data.id);
+
+ return (
+
+
+
+ Cutscene
+ Scene
+
+
{data.id}
+
+ {data.cutsceneId || '(no cutscene ID)'}
+
+
+
+ );
+});
diff --git a/dialogEditor/src/components/nodes/EndNode.tsx b/dialogEditor/src/components/nodes/EndNode.tsx
new file mode 100644
index 0000000..11bd1e3
--- /dev/null
+++ b/dialogEditor/src/components/nodes/EndNode.tsx
@@ -0,0 +1,20 @@
+import { memo } from 'react';
+import { Handle, Position, NodeProps } from '@xyflow/react';
+import { EndNode as EndNodeData } from '../../types/dialogue';
+import styles from './nodes.module.css';
+
+export const EndNode = memo(({ data, selected }: NodeProps & { data: EndNodeData }) => {
+ return (
+
+ );
+});
diff --git a/dialogEditor/src/components/nodes/LineNode.tsx b/dialogEditor/src/components/nodes/LineNode.tsx
new file mode 100644
index 0000000..bdb92a3
--- /dev/null
+++ b/dialogEditor/src/components/nodes/LineNode.tsx
@@ -0,0 +1,66 @@
+import { memo } from 'react';
+import { Handle, Position, NodeProps } from '@xyflow/react';
+import { LineNode as LineNodeData } from '../../types/dialogue';
+import { useValidation } from '../../hooks/useValidation';
+import { useDialogueStore } from '../../store/dialogueStore';
+import { MAIN_CHARACTER } from '../../constants/characters';
+import styles from './nodes.module.css';
+
+export const LineNode = memo(({ data, selected }: NodeProps & { data: LineNodeData }) => {
+ const { hasError, hasWarning } = useValidation();
+ const startId = useDialogueStore(s => {
+ const id = s.selectedDialogueId;
+ return s.file?.dialogues.find(d => d.id === id)?.start;
+ });
+
+ const isMain = data.speaker === MAIN_CHARACTER;
+ const isStart = data.id === startId;
+ const error = hasError(data.id);
+ const warning = !error && hasWarning(data.id);
+
+ const triggers = [
+ data.chatBubble && `💬 ${data.chatBubble}`,
+ data.questUnlock && `🔓 quest`,
+ data.objectiveComplete && `✅ obj`,
+ data.objectiveVisible && `👁 obj`,
+ data.questFail && `❌ quest`,
+ data.questComplete && `🏁 quest`,
+ data.luaCallback && `⚡ lua`,
+ ].filter(Boolean);
+
+ return (
+
+ {isStart &&
▶ START
}
+
+
+ {data.speaker || '(no speaker)'}
+ Line
+
+
{data.id}
+
+ {data.text ? (
+ {data.text}
+ ) : (
+ (empty)
+ )}
+
+ {triggers.length > 0 && (
+
+ {triggers.map((t, i) => (
+ {t}
+ ))}
+
+ )}
+
+
+ );
+});
diff --git a/dialogEditor/src/components/nodes/SetFlagNode.tsx b/dialogEditor/src/components/nodes/SetFlagNode.tsx
new file mode 100644
index 0000000..89c9249
--- /dev/null
+++ b/dialogEditor/src/components/nodes/SetFlagNode.tsx
@@ -0,0 +1,43 @@
+import { memo } from 'react';
+import { Handle, Position, NodeProps } from '@xyflow/react';
+import { SetFlagNode as SetFlagNodeData } from '../../types/dialogue';
+import { useValidation } from '../../hooks/useValidation';
+import styles from './nodes.module.css';
+
+export const SetFlagNode = memo(({ data, selected }: NodeProps & { data: SetFlagNodeData }) => {
+ const { hasError, hasWarning } = useValidation();
+ const error = hasError(data.id);
+ const warning = !error && hasWarning(data.id);
+
+ return (
+
+
+
+ Set Flag
+ Flag
+
+
{data.id}
+
+ {data.effects.map((e, i) => (
+
+ {e.flag}
+ =
+ {String(e.value)}
+
+ ))}
+ {data.effects.length === 0 && (
+
(no effects)
+ )}
+
+
+
+ );
+});
diff --git a/dialogEditor/src/components/nodes/nodeTypes.ts b/dialogEditor/src/components/nodes/nodeTypes.ts
new file mode 100644
index 0000000..dd4146e
--- /dev/null
+++ b/dialogEditor/src/components/nodes/nodeTypes.ts
@@ -0,0 +1,15 @@
+import { LineNode } from './LineNode';
+import { ChoiceNode } from './ChoiceNode';
+import { ConditionNode } from './ConditionNode';
+import { SetFlagNode } from './SetFlagNode';
+import { CutsceneStartNode } from './CutsceneStartNode';
+import { EndNode } from './EndNode';
+
+export const nodeTypes = {
+ Line: LineNode,
+ Choice: ChoiceNode,
+ Condition: ConditionNode,
+ SetFlag: SetFlagNode,
+ CutsceneStart: CutsceneStartNode,
+ End: EndNode,
+} as const;
diff --git a/dialogEditor/src/components/nodes/nodes.module.css b/dialogEditor/src/components/nodes/nodes.module.css
new file mode 100644
index 0000000..87ac285
--- /dev/null
+++ b/dialogEditor/src/components/nodes/nodes.module.css
@@ -0,0 +1,253 @@
+.node {
+ border-radius: 6px;
+ border: 2px solid transparent;
+ background: #1e1e2e;
+ color: #cdd6f4;
+ font-family: 'Segoe UI', system-ui, sans-serif;
+ font-size: 12px;
+ min-width: 220px;
+ max-width: 260px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ position: relative;
+ cursor: pointer;
+}
+
+.node.selected {
+ border-color: #89b4fa;
+ box-shadow: 0 0 0 3px rgba(137,180,250,0.25);
+}
+
+.node.hasError {
+ border-left: 4px solid #f38ba8;
+}
+
+.node.hasWarning {
+ border-left: 4px solid #f9e2af;
+}
+
+/* ---- Header colors ---- */
+.nodeHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 10px;
+ border-radius: 4px 4px 0 0;
+}
+
+.lineNode.mainChar .nodeHeader { background: #1e66f5; }
+.lineNode.otherChar .nodeHeader { background: #40a02b; }
+.choiceNode .nodeHeader { background: #df8e1d; }
+.conditionNode .nodeHeader { background: #8839ef; }
+.setFlagNode .nodeHeader { background: #179299; }
+.cutsceneNode .nodeHeader { background: #4a4a5a; }
+.endNode .nodeHeader { display: none; }
+
+.speakerName {
+ font-weight: 600;
+ font-size: 12px;
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 160px;
+}
+
+.nodeType {
+ font-size: 10px;
+ color: rgba(255,255,255,0.7);
+ font-weight: 500;
+ flex-shrink: 0;
+}
+
+.nodeId {
+ font-size: 10px;
+ color: #6c7086;
+ padding: 2px 10px 0;
+ font-family: monospace;
+}
+
+/* ---- Body ---- */
+.nodeBody {
+ padding: 6px 10px 8px;
+ min-height: 32px;
+}
+
+.textSnippet {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ line-height: 1.4;
+ color: #cdd6f4;
+}
+
+.emptyText {
+ color: #6c7086;
+ font-style: italic;
+}
+
+/* ---- Trigger pills ---- */
+.triggerRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ padding: 0 8px 7px;
+}
+
+.triggerPill {
+ background: rgba(255,255,255,0.08);
+ border-radius: 3px;
+ padding: 1px 5px;
+ font-size: 10px;
+ color: #a6adc8;
+}
+
+/* ---- Choice rows ---- */
+.choiceRow {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 0;
+ border-top: 1px solid rgba(255,255,255,0.06);
+}
+
+.choiceKindBadge {
+ font-size: 9px;
+ font-weight: 700;
+ padding: 1px 5px;
+ border-radius: 3px;
+ flex-shrink: 0;
+ text-transform: uppercase;
+}
+
+.kindMain {
+ background: #fe640b;
+ color: #fff;
+}
+
+.kindOptional {
+ background: transparent;
+ border: 1px solid #fe640b;
+ color: #fe640b;
+}
+
+.choiceText {
+ font-size: 11px;
+ color: #cdd6f4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+/* ---- Condition rows ---- */
+.conditionRow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 0;
+}
+
+.flagPill {
+ background: rgba(137,180,250,0.15);
+ color: #89b4fa;
+ border-radius: 3px;
+ padding: 1px 5px;
+ font-size: 10px;
+ font-family: monospace;
+}
+
+.opBadge {
+ color: #cba6f7;
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.valuePill {
+ background: rgba(166,227,161,0.15);
+ color: #a6e3a1;
+ border-radius: 3px;
+ padding: 1px 5px;
+ font-size: 10px;
+ font-family: monospace;
+}
+
+/* ---- Condition handles ---- */
+.conditionHandles {
+ display: flex;
+ justify-content: space-between;
+ padding: 2px 14px 6px;
+}
+
+.trueLabel {
+ font-size: 9px;
+ font-weight: 700;
+ color: #a6e3a1;
+ text-transform: uppercase;
+ margin-left: 12%;
+}
+
+.falseLabel {
+ font-size: 9px;
+ font-weight: 700;
+ color: #f38ba8;
+ text-transform: uppercase;
+ margin-right: 12%;
+}
+
+/* ---- End node ---- */
+.endNode {
+ min-width: 100px;
+ max-width: 120px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: #2a0e14;
+ border: 2px solid #f38ba8;
+ border-radius: 50px;
+ padding: 10px 16px;
+}
+
+.endLabel {
+ font-size: 14px;
+ font-weight: 700;
+ color: #f38ba8;
+ letter-spacing: 2px;
+}
+
+/* ---- Start badge ---- */
+.startBadge {
+ position: absolute;
+ top: -22px;
+ left: 10px;
+ background: #a6e3a1;
+ color: #1e1e2e;
+ font-size: 10px;
+ font-weight: 700;
+ padding: 2px 8px;
+ border-radius: 4px 4px 0 0;
+}
+
+/* ---- Handles ---- */
+.handle {
+ width: 10px;
+ height: 10px;
+ background: #6c7086;
+ border: 2px solid #1e1e2e;
+}
+
+.trueHandle {
+ background: #a6e3a1 !important;
+}
+
+.falseHandle {
+ background: #f38ba8 !important;
+}
+
+.choiceHandle {
+ width: 8px;
+ height: 8px;
+ background: #fab387 !important;
+ border: 2px solid #1e1e2e;
+ position: absolute !important;
+}
diff --git a/dialogEditor/src/constants/characters.ts b/dialogEditor/src/constants/characters.ts
new file mode 100644
index 0000000..89e3b9d
--- /dev/null
+++ b/dialogEditor/src/constants/characters.ts
@@ -0,0 +1,23 @@
+import { CharacterPreset } from '../types/dialogue';
+
+export const MAIN_CHARACTER = 'Бекзат';
+export const PHONE_PORTRAIT = 'resources/dialogue/portrait_phone.png';
+
+export const CHARACTER_PRESETS: CharacterPreset[] = [
+ { label: 'Бекзат', portrait: 'resources/dialogue/portrait_hero_neutral.png' },
+ { label: 'Аида Джаныбекова', portrait: 'resources/dialogue/portrait_teacher.png' },
+ { label: 'Айпери', portrait: 'resources/dialogue/portrait_aiperi.png' },
+ { label: 'Призрак', portrait: 'resources/dialogue/portrait_ghost.png' },
+ { label: 'Алик', portrait: 'resources/dialogue/portrait_student_boy.png' },
+ { label: 'Студент', portrait: 'resources/dialogue/portrait_student_boy.png' },
+ { label: 'Студентка', portrait: 'resources/dialogue/portrait_student_girl.png' },
+ { label: 'Бермет', portrait: 'resources/dialogue/portrait_student_girl.png' },
+ { label: 'Алтынай', portrait: 'resources/dialogue/portrait_student_girl.png' },
+];
+
+export const CUSTOM_CHARACTER_LABEL = '(custom)';
+
+export function getPortraitForSpeaker(speaker: string): string {
+ const preset = CHARACTER_PRESETS.find(p => p.label === speaker);
+ return preset?.portrait ?? '';
+}
diff --git a/dialogEditor/src/hooks/useAutoSave.ts b/dialogEditor/src/hooks/useAutoSave.ts
new file mode 100644
index 0000000..5735b23
--- /dev/null
+++ b/dialogEditor/src/hooks/useAutoSave.ts
@@ -0,0 +1,31 @@
+import { useState, useEffect, useRef } from 'react';
+
+export function useAutoSave(
+ value: string,
+ onSave: (value: string) => void,
+ delay = 600
+): [string, (v: string) => void] {
+ const [local, setLocal] = useState(value);
+ const timerRef = useRef>(undefined);
+ const savedValueRef = useRef(value);
+
+ useEffect(() => {
+ if (value !== savedValueRef.current) {
+ savedValueRef.current = value;
+ setLocal(value);
+ }
+ }, [value]);
+
+ const handleChange = (v: string) => {
+ setLocal(v);
+ clearTimeout(timerRef.current);
+ timerRef.current = setTimeout(() => {
+ savedValueRef.current = v;
+ onSave(v);
+ }, delay);
+ };
+
+ useEffect(() => () => clearTimeout(timerRef.current), []);
+
+ return [local, handleChange];
+}
diff --git a/dialogEditor/src/hooks/useValidation.ts b/dialogEditor/src/hooks/useValidation.ts
new file mode 100644
index 0000000..3289391
--- /dev/null
+++ b/dialogEditor/src/hooks/useValidation.ts
@@ -0,0 +1,28 @@
+import { useMemo } from 'react';
+import { useDialogueStore } from '../store/dialogueStore';
+import { ValidationIssue } from '../types/dialogue';
+
+export function useValidation(): {
+ issuesByNodeId: Record;
+ hasError: (nodeId: string) => boolean;
+ hasWarning: (nodeId: string) => boolean;
+} {
+ const issues = useDialogueStore(s => s.validationIssues);
+
+ const issuesByNodeId = useMemo(() => {
+ const map: Record = {};
+ for (const issue of issues) {
+ if (!map[issue.nodeId]) map[issue.nodeId] = [];
+ map[issue.nodeId].push(issue);
+ }
+ return map;
+ }, [issues]);
+
+ const hasError = (nodeId: string) =>
+ issuesByNodeId[nodeId]?.some(i => i.severity === 'error') ?? false;
+
+ const hasWarning = (nodeId: string) =>
+ issuesByNodeId[nodeId]?.some(i => i.severity === 'warning') ?? false;
+
+ return { issuesByNodeId, hasError, hasWarning };
+}
diff --git a/dialogEditor/src/index.css b/dialogEditor/src/index.css
new file mode 100644
index 0000000..fe848ed
--- /dev/null
+++ b/dialogEditor/src/index.css
@@ -0,0 +1,31 @@
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+html, body, #root {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
+ background: #1e1e2e;
+ color: #cdd6f4;
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: #181825;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #45475a;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #585b70;
+}
diff --git a/dialogEditor/src/main.tsx b/dialogEditor/src/main.tsx
new file mode 100644
index 0000000..64c40c5
--- /dev/null
+++ b/dialogEditor/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import '@xyflow/react/dist/style.css';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/dialogEditor/src/store/dialogueStore.ts b/dialogEditor/src/store/dialogueStore.ts
new file mode 100644
index 0000000..240ee95
--- /dev/null
+++ b/dialogEditor/src/store/dialogueStore.ts
@@ -0,0 +1,291 @@
+import { create } from 'zustand';
+import { immer } from 'zustand/middleware/immer';
+import {
+ DialogueFile,
+ Dialogue,
+ DialogueNode,
+ ValidationIssue,
+ EndNode,
+} from '../types/dialogue';
+import { computeLayout, PositionMap } from '../utils/layout';
+import { validateDialogue } from '../utils/validation';
+import { applyMobileTransforms } from '../utils/mobileMode';
+import { serializeDialogueFile, triggerDownload } from '../utils/fileIO';
+import { makeNodeId } from '../utils/idGen';
+
+interface NodePosition { x: number; y: number }
+
+interface DialogueStore {
+ file: DialogueFile | null;
+ fileName: string;
+ selectedDialogueId: string | null;
+ selectedNodeId: string | null;
+ positions: Record>;
+ playModeActive: boolean;
+ playState: { currentNodeId: string; flags: Record } | null;
+ persistentFlags: Record;
+ validationIssues: ValidationIssue[];
+
+ loadFile: (file: DialogueFile, name: string) => void;
+ exportFile: () => void;
+
+ selectDialogue: (id: string) => void;
+ createDialogue: (id: string) => void;
+ deleteDialogue: (id: string) => void;
+ duplicateDialogue: (id: string) => void;
+ setDialogueMobileMode: (id: string, value: boolean) => void;
+
+ selectNode: (id: string | null) => void;
+ updateNode: (dialogueId: string, nodeId: string, patch: Partial) => void;
+ addNode: (dialogueId: string, node: DialogueNode, afterNodeId?: string) => void;
+ deleteNode: (dialogueId: string, nodeId: string) => void;
+ setNodePosition: (dialogueId: string, nodeId: string, pos: NodePosition) => void;
+ applyAutoLayout: (dialogueId: string) => void;
+
+ startPlay: () => void;
+ stopPlay: () => void;
+ advancePlay: (nextNodeId: string) => void;
+ setPlayFlag: (flag: string, value: number | string) => void;
+ resetFlags: () => void;
+}
+
+function getDialogue(file: DialogueFile | null, id: string | null): Dialogue | undefined {
+ if (!file || !id) return undefined;
+ return file.dialogues.find(d => d.id === id);
+}
+
+function revalidate(state: DialogueStore): void {
+ const d = getDialogue(state.file, state.selectedDialogueId);
+ state.validationIssues = d ? validateDialogue(d) : [];
+}
+
+export const useDialogueStore = create()(
+ immer((set, get) => ({
+ file: null,
+ fileName: 'dialogues.json',
+ selectedDialogueId: null,
+ selectedNodeId: null,
+ positions: {},
+ playModeActive: false,
+ playState: null,
+ persistentFlags: {},
+ validationIssues: [],
+
+ loadFile: (file, name) => set(state => {
+ state.file = file;
+ state.fileName = name;
+ state.selectedDialogueId = file.dialogues[0]?.id ?? null;
+ state.selectedNodeId = null;
+ state.positions = {};
+ state.playModeActive = false;
+ state.playState = null;
+ revalidate(state);
+ }),
+
+ exportFile: () => {
+ const { file, fileName } = get();
+ if (!file) return;
+ const clone = structuredClone(file);
+ clone.dialogues = clone.dialogues.map(d =>
+ d.mobileMode ? applyMobileTransforms(d) : d
+ );
+ const json = serializeDialogueFile(clone);
+ triggerDownload(json, fileName);
+ },
+
+ selectDialogue: (id) => set(state => {
+ state.selectedDialogueId = id;
+ state.selectedNodeId = null;
+ revalidate(state);
+ }),
+
+ createDialogue: (id) => set(state => {
+ if (!state.file) return;
+ const endNode: EndNode = { id: 'end_1', type: 'End' };
+ const newDialogue: Dialogue = {
+ id,
+ start: 'line_1',
+ nodes: [
+ { id: 'line_1', type: 'Line', speaker: 'Бекзат', portrait: 'resources/dialogue/portrait_hero_neutral.png', text: '', next: 'end_1' },
+ endNode,
+ ],
+ };
+ state.file.dialogues.push(newDialogue);
+ state.selectedDialogueId = id;
+ state.selectedNodeId = null;
+ revalidate(state);
+ }),
+
+ duplicateDialogue: (id) => {
+ // Read raw data outside immer so structuredClone works on plain objects
+ const { file, positions } = get();
+ if (!file) return;
+ const source = file.dialogues.find(d => d.id === id);
+ if (!source) return;
+
+ const existingIds = new Set(file.dialogues.map(d => d.id));
+ const match = source.id.match(/^(.*?)(\d+)$/);
+ let newId: string;
+ if (match) {
+ const base = match[1];
+ const numStr = match[2];
+ let n = parseInt(numStr, 10) + 1;
+ const pad = numStr.length;
+ while (existingIds.has(`${base}${String(n).padStart(pad, '0')}`)) n++;
+ newId = `${base}${String(n).padStart(pad, '0')}`;
+ } else {
+ let n = 2;
+ while (existingIds.has(`${source.id}_${n}`)) n++;
+ newId = `${source.id}_${n}`;
+ }
+
+ const clonedDialogue = structuredClone(source);
+ clonedDialogue.id = newId;
+ const clonedPositions = positions[id] ? structuredClone(positions[id]) : undefined;
+
+ set(state => {
+ state.file!.dialogues.push(clonedDialogue);
+ if (clonedPositions) state.positions[newId] = clonedPositions;
+ state.selectedDialogueId = newId;
+ state.selectedNodeId = null;
+ revalidate(state);
+ });
+ },
+
+ deleteDialogue: (id) => set(state => {
+ if (!state.file) return;
+ state.file.dialogues = state.file.dialogues.filter(d => d.id !== id);
+ if (state.selectedDialogueId === id) {
+ state.selectedDialogueId = state.file.dialogues[0]?.id ?? null;
+ state.selectedNodeId = null;
+ }
+ delete state.positions[id];
+ revalidate(state);
+ }),
+
+ setDialogueMobileMode: (id, value) => set(state => {
+ if (!state.file) return;
+ const d = state.file.dialogues.find(x => x.id === id);
+ if (d) d.mobileMode = value;
+ }),
+
+ selectNode: (id) => set(state => {
+ state.selectedNodeId = id;
+ }),
+
+ updateNode: (dialogueId, nodeId, patch) => set(state => {
+ if (!state.file) return;
+ const d = state.file.dialogues.find(x => x.id === dialogueId);
+ if (!d) return;
+ const idx = d.nodes.findIndex(n => n.id === nodeId);
+ if (idx === -1) return;
+ Object.assign(d.nodes[idx], patch);
+ revalidate(state);
+ }),
+
+ addNode: (dialogueId, node, afterNodeId) => set(state => {
+ if (!state.file) return;
+ const d = state.file.dialogues.find(x => x.id === dialogueId);
+ if (!d) return;
+
+ // Ensure unique ID
+ const existingIds = new Set(d.nodes.map(n => n.id));
+ if (existingIds.has(node.id)) {
+ node = { ...node, id: makeNodeId(node.type, existingIds) };
+ }
+
+ if (afterNodeId) {
+ const idx = d.nodes.findIndex(n => n.id === afterNodeId);
+ if (idx !== -1) {
+ const prev = d.nodes[idx];
+ // Wire: prev.next → new node → old prev.next
+ if (
+ prev.type === 'Line' ||
+ prev.type === 'SetFlag' ||
+ prev.type === 'CutsceneStart'
+ ) {
+ const oldNext = prev.next;
+ (d.nodes[idx] as typeof prev).next = node.id;
+ if (
+ (node.type === 'Line' ||
+ node.type === 'SetFlag' ||
+ node.type === 'CutsceneStart') &&
+ oldNext
+ ) {
+ (node as typeof prev).next = oldNext;
+ }
+ }
+ d.nodes.splice(idx + 1, 0, node);
+ } else {
+ d.nodes.push(node);
+ }
+ } else {
+ d.nodes.push(node);
+ }
+
+ // Place new node just below the anchor node in the graph
+ const anchorId = afterNodeId ?? null;
+ if (anchorId) {
+ const anchorPos = state.positions[dialogueId]?.[anchorId];
+ if (anchorPos) {
+ if (!state.positions[dialogueId]) state.positions[dialogueId] = {};
+ state.positions[dialogueId][node.id] = { x: anchorPos.x, y: anchorPos.y + 200 };
+ }
+ }
+
+ revalidate(state);
+ }),
+
+ deleteNode: (dialogueId, nodeId) => set(state => {
+ if (!state.file) return;
+ const d = state.file.dialogues.find(x => x.id === dialogueId);
+ if (!d) return;
+ d.nodes = d.nodes.filter(n => n.id !== nodeId);
+ if (state.selectedNodeId === nodeId) state.selectedNodeId = null;
+ delete state.positions[dialogueId]?.[nodeId];
+ revalidate(state);
+ }),
+
+ setNodePosition: (dialogueId, nodeId, pos) => set(state => {
+ if (!state.positions[dialogueId]) state.positions[dialogueId] = {};
+ state.positions[dialogueId][nodeId] = pos;
+ }),
+
+ applyAutoLayout: (dialogueId) => set(state => {
+ if (!state.file) return;
+ const d = state.file.dialogues.find(x => x.id === dialogueId);
+ if (!d) return;
+ const layout: PositionMap = computeLayout(d.nodes);
+ if (!state.positions[dialogueId]) state.positions[dialogueId] = {};
+ Object.assign(state.positions[dialogueId], layout);
+ }),
+
+ startPlay: () => set(state => {
+ const d = getDialogue(state.file, state.selectedDialogueId);
+ if (!d) return;
+ state.playModeActive = true;
+ state.playState = { currentNodeId: d.start, flags: { ...state.persistentFlags } };
+ }),
+
+ stopPlay: () => set(state => {
+ state.playModeActive = false;
+ state.playState = null;
+ }),
+
+ advancePlay: (nextNodeId) => set(state => {
+ if (!state.playState) return;
+ state.playState.currentNodeId = nextNodeId;
+ }),
+
+ setPlayFlag: (flag, value) => set(state => {
+ if (!state.playState) return;
+ state.playState.flags[flag] = value;
+ state.persistentFlags[flag] = value;
+ }),
+
+ resetFlags: () => set(state => {
+ state.persistentFlags = {};
+ if (state.playState) state.playState.flags = {};
+ }),
+ }))
+);
diff --git a/dialogEditor/src/types/dialogue.ts b/dialogEditor/src/types/dialogue.ts
new file mode 100644
index 0000000..0a4ec45
--- /dev/null
+++ b/dialogEditor/src/types/dialogue.ts
@@ -0,0 +1,107 @@
+export type ChatBubble = 'in' | 'out';
+export type ConditionOp = 'Equals' | 'NotEquals' | 'GreaterThan' | 'LessThan';
+export type ChoiceKind = 'Main' | 'Optional';
+export type ValidationSeverity = 'error' | 'warning';
+
+export interface ChoiceOption {
+ id: string;
+ kind: ChoiceKind;
+ text: string;
+ next: string;
+}
+
+export interface ConditionClause {
+ flag: string;
+ op: ConditionOp;
+ value: number | string;
+}
+
+export interface FlagEffect {
+ flag: string;
+ value: number | string;
+}
+
+export interface LineNode {
+ id: string;
+ type: 'Line';
+ speaker: string;
+ portrait: string;
+ text: string;
+ next: string;
+ chatBubble?: ChatBubble;
+ questUnlock?: string;
+ objectiveComplete?: string;
+ objectiveVisible?: string;
+ questFail?: string;
+ questComplete?: string;
+ luaCallback?: string;
+}
+
+export interface ChoiceNode {
+ id: string;
+ type: 'Choice';
+ speaker: string;
+ portrait: string;
+ text: string;
+ choices: ChoiceOption[];
+}
+
+export interface ConditionNode {
+ id: string;
+ type: 'Condition';
+ conditions: ConditionClause[];
+ trueNext: string;
+ falseNext: string;
+}
+
+export interface SetFlagNode {
+ id: string;
+ type: 'SetFlag';
+ effects: FlagEffect[];
+ next: string;
+}
+
+export interface CutsceneStartNode {
+ id: string;
+ type: 'CutsceneStart';
+ cutsceneId: string;
+ next: string;
+}
+
+export interface EndNode {
+ id: string;
+ type: 'End';
+}
+
+export type DialogueNode =
+ | LineNode
+ | ChoiceNode
+ | ConditionNode
+ | SetFlagNode
+ | CutsceneStartNode
+ | EndNode;
+
+export type NodeType = DialogueNode['type'];
+
+export interface Dialogue {
+ id: string;
+ start: string;
+ nodes: DialogueNode[];
+ mobileMode?: boolean;
+}
+
+export interface DialogueFile {
+ dialogues: Dialogue[];
+ cutscenes: unknown[];
+}
+
+export interface ValidationIssue {
+ nodeId: string;
+ severity: ValidationSeverity;
+ message: string;
+}
+
+export interface CharacterPreset {
+ label: string;
+ portrait: string;
+}
diff --git a/dialogEditor/src/utils/fileIO.ts b/dialogEditor/src/utils/fileIO.ts
new file mode 100644
index 0000000..d96d47d
--- /dev/null
+++ b/dialogEditor/src/utils/fileIO.ts
@@ -0,0 +1,32 @@
+import { DialogueFile, Dialogue } from '../types/dialogue';
+
+export function parseDialogueFile(json: string): DialogueFile {
+ const parsed = JSON.parse(json);
+ if (!Array.isArray(parsed.dialogues)) {
+ throw new Error('Invalid file: missing dialogues array');
+ }
+ if (!Array.isArray(parsed.cutscenes)) {
+ parsed.cutscenes = [];
+ }
+ return parsed as DialogueFile;
+}
+
+export function serializeDialogueFile(file: DialogueFile): string {
+ const clone = structuredClone(file);
+ clone.dialogues.forEach((d: Dialogue & { mobileMode?: boolean }) => {
+ delete d.mobileMode;
+ });
+ return JSON.stringify(clone, null, '\t');
+}
+
+export function triggerDownload(content: string, filename: string): void {
+ const blob = new Blob([content], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
diff --git a/dialogEditor/src/utils/idGen.ts b/dialogEditor/src/utils/idGen.ts
new file mode 100644
index 0000000..a7becc1
--- /dev/null
+++ b/dialogEditor/src/utils/idGen.ts
@@ -0,0 +1,14 @@
+import { NodeType } from '../types/dialogue';
+
+export function makeNodeId(type: NodeType, existingIds: Set): string {
+ const base = type.toLowerCase();
+ let n = 1;
+ while (existingIds.has(`${base}_${n}`)) n++;
+ return `${base}_${n}`;
+}
+
+export function makeDialogueId(existingIds: Set): string {
+ let n = 1;
+ while (existingIds.has(`dialog_new${n}`)) n++;
+ return `dialog_new${n}`;
+}
diff --git a/dialogEditor/src/utils/layout.ts b/dialogEditor/src/utils/layout.ts
new file mode 100644
index 0000000..51a2b59
--- /dev/null
+++ b/dialogEditor/src/utils/layout.ts
@@ -0,0 +1,53 @@
+import dagre from '@dagrejs/dagre';
+import { DialogueNode } from '../types/dialogue';
+
+export type PositionMap = Record;
+
+const NODE_WIDTH = 260;
+const NODE_HEIGHT = 130;
+
+export function computeLayout(nodes: DialogueNode[]): PositionMap {
+ const g = new dagre.graphlib.Graph();
+ g.setDefaultEdgeLabel(() => ({}));
+ g.setGraph({ rankdir: 'TB', ranksep: 60, nodesep: 50 });
+
+ nodes.forEach(n => {
+ const h = n.type === 'Choice'
+ ? NODE_HEIGHT + Math.max(0, n.choices.length - 1) * 28
+ : NODE_HEIGHT;
+ g.setNode(n.id, { width: NODE_WIDTH, height: h });
+ });
+
+ nodes.forEach(n => {
+ switch (n.type) {
+ case 'Line':
+ case 'SetFlag':
+ case 'CutsceneStart':
+ if (n.next) g.setEdge(n.id, n.next);
+ break;
+ case 'Choice':
+ n.choices.forEach(c => { if (c.next) g.setEdge(n.id, c.next); });
+ break;
+ case 'Condition':
+ if (n.trueNext) g.setEdge(n.id, n.trueNext);
+ if (n.falseNext) g.setEdge(n.id, n.falseNext);
+ break;
+ }
+ });
+
+ dagre.layout(g);
+
+ const positions: PositionMap = {};
+ nodes.forEach(n => {
+ const pos = g.node(n.id);
+ if (pos) {
+ const h = n.type === 'Choice'
+ ? NODE_HEIGHT + Math.max(0, n.choices.length - 1) * 28
+ : NODE_HEIGHT;
+ positions[n.id] = { x: pos.x - NODE_WIDTH / 2, y: pos.y - h / 2 };
+ } else {
+ positions[n.id] = { x: 0, y: 0 };
+ }
+ });
+ return positions;
+}
diff --git a/dialogEditor/src/utils/mobileMode.ts b/dialogEditor/src/utils/mobileMode.ts
new file mode 100644
index 0000000..bd80505
--- /dev/null
+++ b/dialogEditor/src/utils/mobileMode.ts
@@ -0,0 +1,20 @@
+import { Dialogue, DialogueNode } from '../types/dialogue';
+import { MAIN_CHARACTER, PHONE_PORTRAIT } from '../constants/characters';
+
+export function applyMobileTransforms(dialogue: Dialogue): Dialogue {
+ const clone = structuredClone(dialogue);
+ clone.nodes = clone.nodes.map((node): DialogueNode => {
+ if (node.type === 'Line') {
+ return {
+ ...node,
+ portrait: PHONE_PORTRAIT,
+ chatBubble: node.chatBubble ?? (node.speaker === MAIN_CHARACTER ? 'out' : 'in'),
+ };
+ }
+ if (node.type === 'Choice') {
+ return { ...node, portrait: PHONE_PORTRAIT };
+ }
+ return node;
+ });
+ return clone;
+}
diff --git a/dialogEditor/src/utils/validation.ts b/dialogEditor/src/utils/validation.ts
new file mode 100644
index 0000000..fe99040
--- /dev/null
+++ b/dialogEditor/src/utils/validation.ts
@@ -0,0 +1,108 @@
+import { Dialogue, DialogueNode, ValidationIssue } from '../types/dialogue';
+
+function getOutgoingNodeIds(node: DialogueNode): string[] {
+ switch (node.type) {
+ case 'Line':
+ case 'SetFlag':
+ case 'CutsceneStart':
+ return node.next ? [node.next] : [];
+ case 'Choice':
+ return node.choices.map(c => c.next).filter(Boolean);
+ case 'Condition':
+ return [node.trueNext, node.falseNext].filter(Boolean);
+ case 'End':
+ return [];
+ }
+}
+
+function reachableFrom(startId: string, nodeMap: Map): Set {
+ const visited = new Set();
+ const stack = [startId];
+ while (stack.length > 0) {
+ const id = stack.pop()!;
+ if (visited.has(id)) continue;
+ visited.add(id);
+ const node = nodeMap.get(id);
+ if (!node) continue;
+ for (const next of getOutgoingNodeIds(node)) {
+ if (!visited.has(next)) stack.push(next);
+ }
+ }
+ return visited;
+}
+
+export function validateDialogue(dialogue: Dialogue): ValidationIssue[] {
+ const issues: ValidationIssue[] = [];
+ const nodeMap = new Map();
+ const duplicates = new Set();
+
+ // 1. Duplicate IDs
+ for (const node of dialogue.nodes) {
+ if (nodeMap.has(node.id)) {
+ duplicates.add(node.id);
+ issues.push({ nodeId: node.id, severity: 'error', message: `Duplicate node ID: "${node.id}"` });
+ } else {
+ nodeMap.set(node.id, node);
+ }
+ }
+
+ // 2. Missing start node
+ if (dialogue.start && !nodeMap.has(dialogue.start)) {
+ issues.push({ nodeId: '__dialogue__', severity: 'error', message: `Start node "${dialogue.start}" does not exist` });
+ }
+
+ // 3. Broken references
+ for (const node of dialogue.nodes) {
+ if (duplicates.has(node.id)) continue;
+
+ if ((node.type === 'Line' || node.type === 'SetFlag' || node.type === 'CutsceneStart') && node.next) {
+ if (!nodeMap.has(node.next)) {
+ issues.push({ nodeId: node.id, severity: 'error', message: `"next" points to missing node "${node.next}"` });
+ }
+ }
+
+ if (node.type === 'Choice') {
+ for (const choice of node.choices) {
+ if (choice.next && !nodeMap.has(choice.next)) {
+ issues.push({ nodeId: node.id, severity: 'error', message: `Choice "${choice.text}" points to missing node "${choice.next}"` });
+ }
+ }
+ }
+
+ if (node.type === 'Condition') {
+ if (node.trueNext && !nodeMap.has(node.trueNext)) {
+ issues.push({ nodeId: node.id, severity: 'error', message: `"trueNext" points to missing node "${node.trueNext}"` });
+ }
+ if (node.falseNext && !nodeMap.has(node.falseNext)) {
+ issues.push({ nodeId: node.id, severity: 'error', message: `"falseNext" points to missing node "${node.falseNext}"` });
+ }
+ }
+ }
+
+ // 4. Reachability from start
+ if (dialogue.start && nodeMap.has(dialogue.start)) {
+ const reachable = reachableFrom(dialogue.start, nodeMap);
+
+ // No End reachable
+ const hasEnd = [...reachable].some(id => nodeMap.get(id)?.type === 'End');
+ if (!hasEnd) {
+ issues.push({ nodeId: dialogue.start, severity: 'warning', message: 'No End node is reachable from the start' });
+ }
+
+ // Orphaned nodes
+ for (const node of dialogue.nodes) {
+ if (!reachable.has(node.id) && !duplicates.has(node.id)) {
+ issues.push({ nodeId: node.id, severity: 'warning', message: 'Node is not reachable from the start' });
+ }
+ }
+ }
+
+ // 5. Empty text warnings
+ for (const node of dialogue.nodes) {
+ if ((node.type === 'Line' || node.type === 'Choice') && !node.text.trim()) {
+ issues.push({ nodeId: node.id, severity: 'warning', message: 'Node has empty text' });
+ }
+ }
+
+ return issues;
+}
diff --git a/dialogEditor/src/vite-env.d.ts b/dialogEditor/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/dialogEditor/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dialogEditor/tsconfig.json b/dialogEditor/tsconfig.json
new file mode 100644
index 0000000..6bfa73a
--- /dev/null
+++ b/dialogEditor/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/dialogEditor/vite.config.ts b/dialogEditor/vite.config.ts
new file mode 100644
index 0000000..0466183
--- /dev/null
+++ b/dialogEditor/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/resources/config2/items.json b/resources/config2/items.json
index a17c554..095fbdc 100644
--- a/resources/config2/items.json
+++ b/resources/config2/items.json
@@ -20,6 +20,13 @@
"description": "Это ключ от учительской, который я получил от Айпери.",
"icon": "resources/w/ui/img/inv/ItemKey001.png",
"selectedIcon": "resources/w/ui/img/inv/ItemSelKey001.png"
+ },
+ {
+ "id": "all_keys",
+ "name": "Ключи от универа",
+ "description": "Это ключи от всех комнат в универе, которые я получил от Айпери.",
+ "icon": "resources/w/ui/img/inv/ItemKey001.png",
+ "selectedIcon": "resources/w/ui/img/inv/ItemSelKey001.png"
},
{
"id": "knife",
diff --git a/resources/dialogue/dorm_dialogues.json b/resources/dialogue/dorm_dialogues.json
index 8eb372b..eb2a628 100644
--- a/resources/dialogue/dorm_dialogues.json
+++ b/resources/dialogue/dorm_dialogues.json
@@ -189,6 +189,7 @@
},
{
"id": "end_1",
+ "luaCallback" : "on_aiperi_dialog_over",
"type": "End"
}
]
diff --git a/resources/dialogue/uni_interior_dialogues.json b/resources/dialogue/uni_interior_dialogues.json
deleted file mode 100644
index cbfa0f5..0000000
--- a/resources/dialogue/uni_interior_dialogues.json
+++ /dev/null
@@ -1,1298 +0,0 @@
-{
- "dialogues": [
- {
- "id": "knife_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Ты куда собрался, Бекзат?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Вот тебе ключ от учительской. Иди туда и забери нож.",
- "next": "line_3",
- "luaCallback" : "dialog_aiperi_give_key"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "knife_dialog002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Ты куда собрался, Бекзат?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "knife_dialog_second001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Куда Айпери убежала?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ладно, я отдам ей нож завтра.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "aiperi_dialog001",
- "start": "line_1",
- "uninterruptible": true,
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Так где нож, Бекзат? Ты мне его когда вернешь?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Извини, но этот нож внезапно мне очень нужен.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Бекзат, какого черта?",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "У меня вопрос, ты знаешь про Бегимай с прошлого курса?",
- "next": "line_5"
- },
- {
- "id": "line_5",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Я слышала она не смогла сдать курсовую, прыгнула с окна и разбилась насмерть.",
- "next": "line_6"
- },
- {
- "id": "line_6",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Менты приезжали, следователи допрашивали ректора, такой скандал был.",
- "next": "line_7"
- },
- {
- "id": "line_7",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "А зачем тебе?",
- "next": "line_8"
- },
- {
- "id": "line_8",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ты не знаешь кого-нибудь, кто с ней был знаком?",
- "next": "line_9"
- },
- {
- "id": "line_9",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "У тебя в общаге живет Алик, в комнате напротив тебя.",
- "next": "line_10"
- },
- {
- "id": "line_10",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Он с того же курса. Можешь у него поспрашивать.",
- "next": "line_11"
- },
- {
- "id": "line_11",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Хорошо, спасибо!",
- "objectiveComplete" : "ghost_lore.ghost_lore_aiperi",
- "questUnlock": "ghost_release",
- "objectiveVisible" : "ghost_lore.ghost_lore_alik",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "aiperi_dialog002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Я не знаю какие у тебя там дела, Бекзат,",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Но ты должен вернуть мне нож поскорее, а то я тебе голову оторву!",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я обещаю, сейчас я один вопрос решу и верну тебе нож.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "aiperi_dialog003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Так где нож, Бекзат? Ты мне его когда вернешь?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я не могу дать тебе нож, он мне нужен чтобы убивать призраков.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Айпери",
- "portrait": "resources/dialogue/portrait_aiperi.png",
- "text": "Что за чушь? Ты со своими компьютерными играми совсем крышей поехал?",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я обещаю, сейчас я один вопрос решу и верну тебе нож.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "door_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Дверь закрыта на ключ.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "door_teacher_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Это дверь в учительскую, и она закрыта. Мне нужно взять ключи у Айпери.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "door_unlock_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Хорошо что эти двери открываются изнутри.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "door_night_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Дверь закрыта на ключ.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Видимо когда я спал, охранник запер дверь.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Это уже не смешно, как я отсюда выберусь?",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "teacher_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Бекзат, не мешай, я занята.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ладно...",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "teacher_dialog002",
- "start": "line_1",
- "uninterruptible": true,
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Бекзат, тебе отдельное задание на модуль.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Подготовь презентацию по теме \"Манас в изложении Жусупа Мамая\".",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Книга лежит в библиотеке, но забирать ее из библиотеки нельзя.",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Как же я буду готовиться к презентации, если книгу забирать нельзя?",
- "next": "line_5"
- },
- {
- "id": "line_5",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Там в библиотеке есть компьютер, напиши презентацию прямо на нем.",
- "next": "line_6"
- },
- {
- "id": "line_6",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ладно...",
- "objectiveComplete" : "study_beginning.study_beginning_task",
- "questUnlock" : "study_project",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "teacher_dialog003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Аида Дженибековна, вы помните Бегимай? Она вам курсовую хотела сдать.",
- "objectiveComplete" : "ghost_lore.ghost_lore_teacher",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Да, она говорила что приносила курсовую, но у меня ее нигде нет.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Если ты найдешь и покажешь мне ее курсовую работу, я выставлю ей оценку.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "teacher_dialog004",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Аида Дженибековна, Бегимай хотела вам курсовую сдать, вы помните?",
- "objectiveComplete" : "ghost_lore.ghost_lore_teacher",
- "next": "line_2"
- },
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я принес вам ее курсовую, можете посмотреть?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Хорошо, давай посмотрим.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Да, это ее курсовая работа, я вижу.",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Там в шкафу лежат зачетки, найди мне зачетку и принеси мне.",
- "next": "line_5"
- },
- {
- "id": "line_5",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Хорошо!",
- "objectiveComplete" : "ghost_coursework.ghost_coursework_mark",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "teacher_dialog005",
- "start": "line_1",
- "uninterruptible": true,
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Аида Дженибековна, Бегимай хотела вам курсовую сдать, вы помните?",
- "objectiveComplete" : "ghost_lore.ghost_lore_teacher",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я принес вам ее зачетку и ее курсовую работу, посмотрите пожалуйста.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Хорошо, давай посмотрим.",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Да, я вижу что курсовая написана хорошо, я ставлю ей максимальный балл.",
- "objectiveComplete" : "ghost_coursework.ghost_coursework_mark",
- "next": "line_5"
- },
- {
- "id": "line_5",
- "type": "Line",
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Вот держи зачетку с оценкой.",
- "objectiveComplete" : "ghost_release.ghost_release_mark",
- "next": "line_6"
- },
- {
- "id": "line_6",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Спасибо!",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "book_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Мне стоит вернуть книгу на место, прежде чем уходить из библиотеки.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Иначе Аида Дженибековна меня убъет.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "ghost_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Куда я попал?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я что, сплю?",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "ghost_dialog002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я вижу какие-то тени!",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Мне лучше держать нож наготове.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_with_ghost001",
- "uninterruptible": true,
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Призрак",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Ты посмел заговорить со мной?",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ты кто?",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Меня зовут Бегимай, мне нужно сдать курсовую по манасоведению.",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Я появляюсь здесь каждую ночь, чтобы сдать курсовую.",
- "next": "line_5"
- },
- {
- "id": "line_5",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "И как я могу заставить тебя уйти отсюда?",
- "next": "line_6"
- },
- {
- "id": "line_6",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Я уйду на покой только когда увиду оценку по курсовой в своей зачетке.",
- "next": "line_7"
- },
- {
- "id": "line_7",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "А до тех пор, я буду появлятся здесь каждую ночь.",
- "next": "line_8"
- },
- {
- "id": "line_8",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ладно, я что-нибудь придумаю.",
- "luaCallback" : "on_first_ghost_dialog_over",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_with_ghost002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Я Бегимай, мне нужно сдать курсовую по манасоведению.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Я появляюсь здесь каждую ночь, чтобы сдать курсовую.",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Я уйду на покой только когда увиду оценку по курсовой в своей зачетке.",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ладно, я что-нибудь придумаю.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_with_ghost003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Эй простыня, посмотри сюда.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Что это?",
- "next": "line_3"
- },
- {
- "id": "line_3",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Это твоя зачетка.",
- "next": "line_4"
- },
- {
- "id": "line_4",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Тебе поставили за курсовую максимальный балл!",
- "next": "line_5"
- },
- {
- "id": "line_5",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Неужели! Наконец-то, мое предназначение исполнено.",
- "next": "line_6"
- },
- {
- "id": "line_6",
- "type": "Line",
- "speaker": "Бегимай",
- "portrait": "resources/dialogue/portrait_ghost.png",
- "text": "Теперь я могу уйти на покой.",
- "next": "line_7"
- },
- {
- "id": "line_7",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Давай, иди отдыхай.",
- "luaCallback" : "on_quest_over",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "note_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Здесь написано: прочитай меня вслух.",
- "next": "line_2"
- },
- {
- "id": "line_2",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "[Читает].",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "book_dialog002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Сначала мне нужно найти книку в библиотеке.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "book_dialog003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Да, вот эта книга! Я возьму ее.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "book_dialog004",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я вернул книгу на место.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "book_dialog005",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "На этой полке лежат самые скучные книги в этом кабинете.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "book_dialog006",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я надеюсь, мне эта книга больше не пригодится.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "computer_dialog001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Это старый библиотечный компьютер, он даже не подключен к интернету.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "computer_dialog002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "От этого компьютера сейчас не будет никакого толку.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "computer_dialog003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Надеюсь мне больше не придется притрагиваться к этому компьютеру.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_report_card001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Тут лежат зачетные книжки студентов.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_report_card002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Здесь лежит зачетка Бегимай. Я пожалуй, возьму ее.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_report_card003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Мне еще рано возвращать зачетку Бегимай обратно в шкаф.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_taxi003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Чтобы заказать такси, я сначала должен выйти на улицу.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_video001",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Ого, пока я залипал в приложении, уже наступила ночь!",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_video002",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я не буду сейчас смотреть видеоролики, давай лучше вернемся в общагу и пойдем спать.",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- },
- {
- "id": "dialog_video003",
- "start": "line_1",
- "nodes": [
- {
- "id": "line_1",
- "type": "Line",
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Мне некогда деградировать сейчас!",
- "next": "end_1"
- },
- {
- "id": "end_1",
- "type": "End"
- }
- ]
- }
- ],
- "cutscenes": [
- {
- "id": "test_cutscene_01",
- "background": "resources/test_cutscene001.png",
- "durationMs": 5000,
- "fadeOutMs": 500,
- "fadeInMs": 500,
- "endFadeOutMs": 500,
- "endFadeInMs": 500,
- "cameraTrack": [
- {
- "durationMs": 3000,
- "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.10, "rotationDeg": 0.0 },
- "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.00, "rotationDeg": 0.0 },
- "easing": "EaseInOutSine"
- },
- {
- "durationMs": 3000,
- "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.0, "rotationDeg": 0.0 },
- "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.1, "rotationDeg": 0.0 },
- "easing": "EaseInOutCubic"
- }
- ],
- "lines": [
- {
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?",
- "durationMs": 3000
- },
- {
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
- "durationMs": 3000
- },
- {
- "speaker": "Аида Дженибековна",
- "portrait": "resources/dialogue/portrait_teacher.png",
- "text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.",
- "durationMs": 2000,
- "background": "resources/test_cutscene001.png"
- }
- ]
- },
- {
- "id": "computer_cutscene001",
- "onFadeInCallback": "on_sleep_cutscene",
- "background": "resources/test_cutscene001.png",
- "durationMs": 5000,
- "fadeOutMs": 500,
- "fadeInMs": 500,
- "endFadeOutMs": 500,
- "endFadeInMs": 500,
- "cameraTrack": [
- {
- "durationMs": 3000,
- "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.10, "rotationDeg": 0.0 },
- "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.00, "rotationDeg": 0.0 },
- "easing": "EaseInOutSine"
- },
- {
- "durationMs": 3000,
- "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.0, "rotationDeg": 0.0 },
- "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.1, "rotationDeg": 0.0 },
- "easing": "EaseInOutCubic"
- }
- ],
- "lines": [
- {
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я начал делать презентацию по книге.",
- "durationMs": 3000
- },
- {
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Книга была такая скучная что я уснул.",
- "durationMs": 3000
- },
- {
- "speaker": "Бекзат",
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "И я проснулся уже ночью",
- "durationMs": 2000,
- "background": "resources/test_cutscene001.png"
- }
- ]
- },
- {
- "id": "darklands_exit001",
- "onFadeInCallback": "on_darklands_over",
- "background": "resources/test_cutscene001.png",
- "durationMs": 5000,
- "fadeOutMs": 0,
- "fadeInMs": 500,
- "endFadeOutMs": 500,
- "endFadeInMs": 500,
- "cameraTrack": [
- {
- "durationMs": 3000,
- "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.10, "rotationDeg": 0.0 },
- "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.00, "rotationDeg": 0.0 },
- "easing": "EaseInOutSine"
- },
- {
- "durationMs": 3000,
- "from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.0, "rotationDeg": 0.0 },
- "to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.1, "rotationDeg": 0.0 },
- "easing": "EaseInOutCubic"
- }
- ],
- "lines": [
- {
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Мгновенно как я упал без сил, что-то сверкнуло.",
- "durationMs": 3000
- },
- {
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Я открыл глаза и понял, что я по-прежнему в универе.",
- "durationMs": 3000
- },
- {
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "Все тело болело, как будто я всю ночь таскал мешки с цементом.",
- "durationMs": 3000
- },
- {
- "portrait": "resources/dialogue/portrait_hero_neutral.png",
- "text": "А еще мне сильно хотелось спать...",
- "durationMs": 2000,
- "background": "resources/test_cutscene001.png"
- }
- ]
- }
- ]
-}
diff --git a/resources/dialogue/uni_interior_dialogues_003.json b/resources/dialogue/uni_interior_dialogues_003.json
new file mode 100644
index 0000000..b1b1bec
--- /dev/null
+++ b/resources/dialogue/uni_interior_dialogues_003.json
@@ -0,0 +1,2091 @@
+{
+ "dialogues": [
+ {
+ "id": "knife_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Ты куда собрался, Бекзат?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Вот тебе ключ от учительской. Иди туда и забери нож.",
+ "next": "line_3",
+ "luaCallback": "dialog_aiperi_give_key"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "knife_dialog002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Ты куда собрался, Бекзат?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "knife_dialog_second001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Куда Айпери убежала?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ладно, я отдам ей нож завтра.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "aiperi_dialog001",
+ "start": "line_1",
+ "uninterruptible": true,
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Так где нож, Бекзат? Ты мне его когда вернешь?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Извини, но этот нож внезапно мне очень нужен.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Бекзат, какого черта?",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "У меня вопрос, ты знаешь про Бегимай с прошлого курса?",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Я слышала она не смогла сдать курсовую, прыгнула с окна и разбилась насмерть.",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Менты приезжали, следователи допрашивали ректора, такой скандал был.",
+ "next": "line_7"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "А зачем тебе?",
+ "next": "line_8"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ты не знаешь кого-нибудь, кто с ней был знаком?",
+ "next": "line_9"
+ },
+ {
+ "id": "line_9",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "У тебя в общаге живет Алик, в комнате напротив тебя.",
+ "next": "line_10"
+ },
+ {
+ "id": "line_10",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Он с того же курса. Можешь у него поспрашивать.",
+ "next": "line_11"
+ },
+ {
+ "id": "line_11",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Хорошо, спасибо!",
+ "objectiveComplete": "ghost_lore.ghost_lore_aiperi",
+ "questUnlock": "ghost_release",
+ "objectiveVisible": "ghost_lore.ghost_lore_alik",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "aiperi_dialog002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Я не знаю какие у тебя там дела, Бекзат,",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Но ты должен вернуть мне нож поскорее, а то я тебе голову оторву!",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я обещаю, сейчас я один вопрос решу и верну тебе нож.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "aiperi_dialog003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Так где нож, Бекзат? Ты мне его когда вернешь?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я не могу дать тебе нож, он мне нужен чтобы убивать призраков.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Что за чушь? Ты со своими компьютерными играми совсем крышей поехал?",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я обещаю, сейчас я один вопрос решу и верну тебе нож.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Дверь закрыта на ключ.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_teacher_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Это дверь в учительскую, и она закрыта. Мне нужно взять ключи у Айпери.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_unlock_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Хорошо что эти двери открываются изнутри.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "door_night_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Дверь закрыта на ключ.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Видимо когда я спал, охранник запер дверь.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Это уже не смешно, как я отсюда выберусь?",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "teacher_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Бекзат, не мешай, я занята.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ладно...",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "teacher_dialog002",
+ "start": "line_1",
+ "uninterruptible": true,
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Бекзат, тебе отдельное задание на модуль.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Подготовь презентацию по теме \"Манас в изложении Жусупа Мамая\".",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Книга лежит в библиотеке, но забирать ее из библиотеки нельзя.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Как же я буду готовиться к презентации, если книгу забирать нельзя?",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Там в библиотеке есть компьютер, напиши презентацию прямо на нем.",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ладно...",
+ "objectiveComplete": "study_beginning.study_beginning_task",
+ "questUnlock": "study_project",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "teacher_dialog003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Аида Дженибековна, вы помните Бегимай? Она вам курсовую хотела сдать.",
+ "objectiveComplete": "ghost_lore.ghost_lore_teacher",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Да, она говорила что приносила курсовую, но у меня ее нигде нет.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Если ты найдешь и покажешь мне ее курсовую работу, я выставлю ей оценку.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "teacher_dialog004",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Аида Дженибековна, Бегимай хотела вам курсовую сдать, вы помните?",
+ "objectiveComplete": "ghost_lore.ghost_lore_teacher",
+ "next": "line_2"
+ },
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я принес вам ее курсовую, можете посмотреть?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Хорошо, давай посмотрим.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Да, это ее курсовая работа, я вижу.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Там в шкафу лежат зачетки, найди мне зачетку и принеси мне.",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Хорошо!",
+ "objectiveComplete": "ghost_coursework.ghost_coursework_mark",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "teacher_dialog005",
+ "start": "line_1",
+ "uninterruptible": true,
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Аида Дженибековна, Бегимай хотела вам курсовую сдать, вы помните?",
+ "objectiveComplete": "ghost_lore.ghost_lore_teacher",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я принес вам ее зачетку и ее курсовую работу, посмотрите пожалуйста.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Хорошо, давай посмотрим.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Да, я вижу что курсовая написана хорошо, я ставлю ей максимальный балл.",
+ "objectiveComplete": "ghost_coursework.ghost_coursework_mark",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Вот держи зачетку с оценкой.",
+ "objectiveComplete": "ghost_release.ghost_release_mark",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Спасибо!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "book_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Мне стоит вернуть книгу на место, прежде чем уходить из библиотеки.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Иначе Аида Дженибековна меня убъет.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "ghost_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Куда я попал?",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я что, сплю?",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "ghost_dialog002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я вижу какие-то тени!",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Мне лучше держать нож наготове.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_with_ghost001",
+ "uninterruptible": true,
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Призрак",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Ты посмел заговорить со мной?",
+ "next": "condition_1"
+ },
+ {
+ "id": "condition_1",
+ "type": "Condition",
+ "conditions": [
+ {
+ "flag": "ghost_aware",
+ "op": "Equals",
+ "value": 1
+ }
+ ],
+ "trueNext": "line_9",
+ "falseNext": "line_2"
+ },
+ {
+ "id": "line_9",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Напомни пожалуйста, кто ты?",
+ "next": "line_10"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ты кто?",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Меня зовут Бегимай, мне нужно сдать курсовую по манасоведению.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Я появляюсь здесь каждую ночь, чтобы сдать курсовую.",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "И как я могу заставить тебя уйти отсюда?",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Я уйду на покой только когда увиду оценку по курсовой в своей зачетке.",
+ "next": "line_7"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "А до тех пор, я буду появлятся здесь каждую ночь.",
+ "next": "line_8"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ладно, я что-нибудь придумаю.",
+ "luaCallback": "",
+ "next": "setflag_1",
+ "questUnlock": "ghost_lore"
+ },
+ {
+ "id": "setflag_1",
+ "type": "SetFlag",
+ "effects": [
+ {
+ "flag": "ghost_aware",
+ "value": 1
+ }
+ ],
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ },
+ {
+ "id": "line_10",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Меня зовут Бегимай, мне нужно сдать курсовую по манасоведению.",
+ "next": "line_11"
+ },
+ {
+ "id": "line_11",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Я появляюсь здесь каждую ночь, чтобы сдать курсовую.",
+ "next": "line_6"
+ }
+ ]
+ },
+ {
+ "id": "dialog_with_ghost002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Я Бегимай, мне нужно сдать курсовую по манасоведению.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Я появляюсь здесь каждую ночь, чтобы сдать курсовую.",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Я уйду на покой только когда увиду оценку по курсовой в своей зачетке.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ладно, я что-нибудь придумаю.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_with_ghost003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Эй простыня, посмотри сюда.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Что это?",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Это твоя зачетка.",
+ "next": "line_4"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Тебе поставили за курсовую максимальный балл!",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Неужели! Наконец-то, мое предназначение исполнено.",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Бегимай",
+ "portrait": "resources/dialogue/portrait_ghost.png",
+ "text": "Теперь я могу уйти на покой.",
+ "next": "line_7"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Давай, иди отдыхай.",
+ "luaCallback": "on_quest_over",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "note_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Здесь написано: прочитай меня вслух.",
+ "next": "line_2"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "[Читает].",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "book_dialog002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Сначала мне нужно найти книку в библиотеке.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "book_dialog003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Да, вот эта книга! Я возьму ее.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "book_dialog004",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я вернул книгу на место.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "book_dialog005",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "На этой полке лежат самые скучные книги в этом кабинете.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "book_dialog006",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я надеюсь, мне эта книга больше не пригодится.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "computer_dialog001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Это старый библиотечный компьютер, он даже не подключен к интернету.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "computer_dialog002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "От этого компьютера сейчас не будет никакого толку.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "computer_dialog003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Надеюсь мне больше не придется притрагиваться к этому компьютеру.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_report_card001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Тут лежат зачетные книжки студентов.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_report_card002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Здесь лежит зачетка Бегимай. Я пожалуй, возьму ее.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_report_card003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Мне еще рано возвращать зачетку Бегимай обратно в шкаф.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_taxi003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Чтобы заказать такси, я сначала должен выйти на улицу.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_video001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ого, пока я залипал в приложении, уже наступила ночь!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_video002",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я не буду сейчас смотреть видеоролики, давай лучше вернемся в общагу и пойдем спать.",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "dialog_video003",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Мне некогда деградировать сейчас!",
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "phone_night_aiperi001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_think.png",
+ "text": "Стоит ли написать Айпери?",
+ "next": "choice_1"
+ },
+ {
+ "id": "choice_1",
+ "type": "Choice",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_think.png",
+ "text": "",
+ "choices": [
+ {
+ "id": "choice_1780657177062",
+ "kind": "Main",
+ "text": "Попросить помощь",
+ "next": "line_2"
+ },
+ {
+ "id": "choice_1780657211405",
+ "kind": "Optional",
+ "text": "Неважно",
+ "next": "end_1"
+ }
+ ]
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Айпери, ты в сети?",
+ "next": "line_11",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_11",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я уже спать собираюсь. Что случилось?",
+ "next": "line_3",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я уснул в универе, и меня заперли тут!",
+ "next": "line_4",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Пхахахх лашара",
+ "next": "line_5",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Это не смешно!",
+ "next": "line_6",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я знаю у тебя есть ключи от всех кабинетов. Ты можешь приехать, открыть библиотеку и выпустить меня?",
+ "next": "line_7",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Время уже позднее.",
+ "next": "line_8",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Там в библиотеке есть мягкий диван. Ляг поспи, а я приеду утром и выпущу тебя.",
+ "next": "line_9",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_9",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Айпери, ну пожалуйста!",
+ "next": "line_10",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_10",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я пойду спать. Доброй ночи!",
+ "next": "setflag_1",
+ "chatBubble": "in"
+ },
+ {
+ "id": "setflag_1",
+ "type": "SetFlag",
+ "effects": [
+ {
+ "flag": "asked_help_locked",
+ "value": 1
+ }
+ ],
+ "next": "end_1"
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ }
+ ]
+ },
+ {
+ "id": "phone_dialog_night_parents001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Стоит ли написать родителям?",
+ "next": "choice_1",
+ "chatBubble": "out"
+ },
+ {
+ "id": "choice_1",
+ "type": "Choice",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "",
+ "choices": [
+ {
+ "id": "choice_1780657619319",
+ "kind": "Main",
+ "text": "Попросить помощь",
+ "next": "line_2"
+ },
+ {
+ "id": "choice_1780657645270",
+ "kind": "Optional",
+ "text": "Неважно",
+ "next": "end_1"
+ }
+ ]
+ },
+ {
+ "id": "end_1",
+ "type": "End"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Привет, ата! Меня заперли в универе ночью!",
+ "next": "line_3",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Отец",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Сынок, ты же знаешь я живу далеко",
+ "next": "line_4",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Отец",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Пока мы приедем, настанет утро!",
+ "next": "line_5",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Да, точно, извнините",
+ "next": "line_6",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Отец",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Напиши своей стартосте, может она придумает что сделать?",
+ "next": "end_1",
+ "chatBubble": "in"
+ }
+ ]
+ },
+ {
+ "id": "phone_morning_aiperi001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Айпери! Ты где сейчас?",
+ "next": "condition_1",
+ "chatBubble": "out"
+ },
+ {
+ "id": "condition_1",
+ "type": "Condition",
+ "conditions": [
+ {
+ "flag": "asked_help_locked",
+ "op": "Equals",
+ "value": 1
+ }
+ ],
+ "trueNext": "line_7",
+ "falseNext": "line_2"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я приехала в универститет чтобы тебя освободить.",
+ "next": "line_8",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я только что приехала в универ. Что случилось?",
+ "next": "line_3",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Я заперт в кабинете. Открой мне дверь пожалуйста!",
+ "next": "line_4",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Как ты там оказался?",
+ "next": "line_5",
+ "chatBubble": "in"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Это долгая история...",
+ "next": "line_6",
+ "chatBubble": "out"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Отойди от двери, я сейчас тебя освобожу!",
+ "next": "end_1",
+ "chatBubble": "in"
+ },
+ {
+ "id": "end_1",
+ "luaCallback" : "on_aiperi_opens_door",
+ "type": "End"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_phone.png",
+ "text": "Спасибо большое!",
+ "next": "line_6",
+ "chatBubble": "out"
+ }
+ ]
+ },
+ {
+ "id": "dialog_aiperi_morning001",
+ "start": "line_1",
+ "nodes": [
+ {
+ "id": "line_1",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Айпери!",
+ "next": "condition_3"
+ },
+ {
+ "id": "condition_3",
+ "type": "Condition",
+ "conditions": [
+ {
+ "flag": "morning_aiperi_talked",
+ "op": "Equals",
+ "value": 1
+ }
+ ],
+ "trueNext": "line_26",
+ "falseNext": "line_27"
+ },
+ {
+ "id": "line_27",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Спасибо большое!",
+ "next": "line_2"
+ },
+ {
+ "id": "line_26",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Я не знаю какие у тебя там дела, Бекзат, но ты должен вернуть мне нож как можно скорее!",
+ "next": "condition_4"
+ },
+ {
+ "id": "condition_4",
+ "type": "Condition",
+ "conditions": [
+ {
+ "flag": "ghost_aware",
+ "op": "Equals",
+ "value": 1
+ },
+ {
+ "flag": "alik_aware",
+ "op": "NotEquals",
+ "value": 1
+ }
+ ],
+ "trueNext": "line_30",
+ "falseNext": "line_28"
+ },
+ {
+ "id": "line_30",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "У меня есть один вопрос.",
+ "next": "line_29"
+ },
+ {
+ "id": "line_28",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Хорошо, я скоро верну тебе твой нож.",
+ "next": "end_1"
+ },
+ {
+ "id": "line_2",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Во-первых, постарайся сделать так, чтобы этого больше не повторялось. Мне вовсе не в кайф вытаскивать твою задницу из проблем.",
+ "luaCallback": "",
+ "next": "line_3"
+ },
+ {
+ "id": "line_3",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Во-вторых, какого хрена ты в универе делаешь ночью?",
+ "next": "choice_1"
+ },
+ {
+ "id": "choice_1",
+ "type": "Choice",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "",
+ "choices": [
+ {
+ "id": "choice_1780659320965",
+ "kind": "Main",
+ "text": "Рассказать про записку",
+ "next": "line_4"
+ },
+ {
+ "id": "choice_1780659326945",
+ "kind": "Optional",
+ "text": "Умолчать",
+ "next": "line_8"
+ }
+ ]
+ },
+ {
+ "id": "line_4",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ко мне попала странная записка, где было написано заклинание. Я прочитал его, и попал в потусторонний мир.",
+ "next": "line_5"
+ },
+ {
+ "id": "line_5",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Ты что опять в свои игры переиграл? Какой потусторонний мир?",
+ "next": "line_6"
+ },
+ {
+ "id": "line_6",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Но это правда! Там все было по-другому, и я видел призраков...",
+ "next": "line_7"
+ },
+ {
+ "id": "line_7",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Тебе еще и не такое привидется. Езжай в общагу и выспись.",
+ "next": "setflag_1"
+ },
+ {
+ "id": "setflag_1",
+ "type": "SetFlag",
+ "effects": [
+ {
+ "flag": "told_aiperi_ghost",
+ "value": 1
+ }
+ ],
+ "next": "line_11"
+ },
+ {
+ "id": "end_1",
+ "luaCallback" : "on_aiperi_morning_dialog_finished",
+ "type": "End"
+ },
+ {
+ "id": "line_8",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Аида Джаныбекова увидела меня в учительской, и заставила писать эссе по этой тупой книге.",
+ "next": "line_9"
+ },
+ {
+ "id": "line_9",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я писал, писал и уснул.",
+ "next": "line_10"
+ },
+ {
+ "id": "line_10",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Дааа, она такая. Видишь как хорошо, что именно ты пошел за ножом, а не я.",
+ "next": "line_11"
+ },
+ {
+ "id": "line_11",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Да, кстати.",
+ "next": "line_12"
+ },
+ {
+ "id": "line_12",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Отдавай нож.",
+ "next": "line_13"
+ },
+ {
+ "id": "line_13",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Да, насчет этого. Боюсь, что я не могу тебе пока что отдать нож.",
+ "next": "line_14"
+ },
+ {
+ "id": "line_14",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Какого черта?",
+ "next": "condition_1"
+ },
+ {
+ "id": "condition_1",
+ "type": "Condition",
+ "conditions": [
+ {
+ "flag": "told_aiperi_ghost",
+ "op": "Equals",
+ "value": 1
+ }
+ ],
+ "trueNext": "line_16",
+ "falseNext": "line_15"
+ },
+ {
+ "id": "line_16",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Оказывается, я могу убивать призраков только этим ножом, без него я буду беззащитен.",
+ "next": "line_17"
+ },
+ {
+ "id": "line_17",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Но ты не переживай, я сделаю кое-какое дело и верну тебе этот нож.",
+ "next": "line_18"
+ },
+ {
+ "id": "line_18",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Постарайся сделать свои дела быстрее, иначе это я тебе голову оторву!",
+ "next": "setflag_3"
+ },
+ {
+ "id": "setflag_3",
+ "type": "SetFlag",
+ "effects": [
+ {
+ "flag": "morning_aiperi_talked",
+ "value": 1
+ }
+ ],
+ "next": "condition_2"
+ },
+ {
+ "id": "condition_2",
+ "type": "Condition",
+ "conditions": [
+ {
+ "flag": "ghost_aware",
+ "op": "Equals",
+ "value": 1
+ }
+ ],
+ "trueNext": "line_20",
+ "falseNext": "end_1"
+ },
+ {
+ "id": "line_20",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Да, и еще кое что.",
+ "next": "line_29"
+ },
+ {
+ "id": "line_29",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ты знаешь про студентку Бегимай с прошлого курса?",
+ "next": "line_19"
+ },
+ {
+ "id": "line_19",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Я слышала она не смогла сдать курсовую по манасоведению, прыгнула с окна и разбилась насмерть.",
+ "next": "line_21"
+ },
+ {
+ "id": "line_21",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Менты приезжали, следователи допрашивали ректора, такой скандал был. Прежнего преподавателя по манасоведению отстранили.",
+ "next": "line_22"
+ },
+ {
+ "id": "line_22",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "Поэтому, с этого года у нас этот предмет ведет Аида Дженибековна.",
+ "next": "line_23"
+ },
+ {
+ "id": "line_23",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Ты не знаешь кого-нибудь, кто был знаком с Бегимай?",
+ "next": "line_24"
+ },
+ {
+ "id": "line_24",
+ "type": "Line",
+ "speaker": "Айпери",
+ "portrait": "resources/dialogue/portrait_aiperi.png",
+ "text": "У тебя в общаге живет Алик, в комнате напротив тебя. Он с того же курса. Можешь у него поспрашивать.",
+ "next": "line_25"
+ },
+ {
+ "id": "line_25",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Спасибо!",
+ "next": "setflag_2"
+ },
+ {
+ "id": "setflag_2",
+ "type": "SetFlag",
+ "effects": [
+ {
+ "flag": "alik_aware",
+ "value": 1
+ }
+ ],
+ "next": "end_1"
+ },
+ {
+ "id": "line_15",
+ "type": "Line",
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Нож мне нужен для кое-каких дел.",
+ "next": "line_17"
+ }
+ ]
+ }
+ ],
+ "cutscenes": [
+ {
+ "id": "test_cutscene_01",
+ "background": "resources/test_cutscene001.png",
+ "durationMs": 5000,
+ "fadeOutMs": 500,
+ "fadeInMs": 500,
+ "endFadeOutMs": 500,
+ "endFadeInMs": 500,
+ "cameraTrack": [
+ {
+ "durationMs": 3000,
+ "from": {
+ "focusX": 0.3,
+ "focusY": 0.5,
+ "zoom": 1.1,
+ "rotationDeg": 0
+ },
+ "to": {
+ "focusX": 0.7,
+ "focusY": 0.5,
+ "zoom": 1,
+ "rotationDeg": 0
+ },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 3000,
+ "from": {
+ "focusX": 0.3,
+ "focusY": 0.5,
+ "zoom": 1,
+ "rotationDeg": 0
+ },
+ "to": {
+ "focusX": 0.7,
+ "focusY": 0.5,
+ "zoom": 1.1,
+ "rotationDeg": 0
+ },
+ "easing": "EaseInOutCubic"
+ }
+ ],
+ "lines": [
+ {
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?",
+ "durationMs": 3000
+ },
+ {
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
+ "durationMs": 3000
+ },
+ {
+ "speaker": "Аида Дженибековна",
+ "portrait": "resources/dialogue/portrait_teacher.png",
+ "text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.",
+ "durationMs": 2000,
+ "background": "resources/test_cutscene001.png"
+ }
+ ]
+ },
+ {
+ "id": "computer_cutscene001",
+ "onFadeInCallback": "on_sleep_cutscene",
+ "background": "resources/test_cutscene001.png",
+ "durationMs": 5000,
+ "fadeOutMs": 500,
+ "fadeInMs": 500,
+ "endFadeOutMs": 500,
+ "endFadeInMs": 500,
+ "cameraTrack": [
+ {
+ "durationMs": 3000,
+ "from": {
+ "focusX": 0.3,
+ "focusY": 0.5,
+ "zoom": 1.1,
+ "rotationDeg": 0
+ },
+ "to": {
+ "focusX": 0.7,
+ "focusY": 0.5,
+ "zoom": 1,
+ "rotationDeg": 0
+ },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 3000,
+ "from": {
+ "focusX": 0.3,
+ "focusY": 0.5,
+ "zoom": 1,
+ "rotationDeg": 0
+ },
+ "to": {
+ "focusX": 0.7,
+ "focusY": 0.5,
+ "zoom": 1.1,
+ "rotationDeg": 0
+ },
+ "easing": "EaseInOutCubic"
+ }
+ ],
+ "lines": [
+ {
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я начал делать презентацию по книге.",
+ "durationMs": 3000
+ },
+ {
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Книга была такая скучная что я уснул.",
+ "durationMs": 3000
+ },
+ {
+ "speaker": "Бекзат",
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "И я проснулся уже ночью",
+ "durationMs": 2000,
+ "background": "resources/test_cutscene001.png"
+ }
+ ]
+ },
+ {
+ "id": "darklands_exit001",
+ "onFadeInCallback": "on_darklands_over",
+ "background": "resources/test_cutscene001.png",
+ "durationMs": 5000,
+ "fadeOutMs": 0,
+ "fadeInMs": 500,
+ "endFadeOutMs": 500,
+ "endFadeInMs": 500,
+ "cameraTrack": [
+ {
+ "durationMs": 3000,
+ "from": {
+ "focusX": 0.3,
+ "focusY": 0.5,
+ "zoom": 1.1,
+ "rotationDeg": 0
+ },
+ "to": {
+ "focusX": 0.7,
+ "focusY": 0.5,
+ "zoom": 1,
+ "rotationDeg": 0
+ },
+ "easing": "EaseInOutSine"
+ },
+ {
+ "durationMs": 3000,
+ "from": {
+ "focusX": 0.3,
+ "focusY": 0.5,
+ "zoom": 1,
+ "rotationDeg": 0
+ },
+ "to": {
+ "focusX": 0.7,
+ "focusY": 0.5,
+ "zoom": 1.1,
+ "rotationDeg": 0
+ },
+ "easing": "EaseInOutCubic"
+ }
+ ],
+ "lines": [
+ {
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Мгновенно как я упал без сил, что-то сверкнуло.",
+ "durationMs": 3000
+ },
+ {
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Я открыл глаза и понял, что я по-прежнему в универе.",
+ "durationMs": 3000
+ },
+ {
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "Все тело болело, как будто я всю ночь таскал мешки с цементом.",
+ "durationMs": 3000
+ },
+ {
+ "portrait": "resources/dialogue/portrait_hero_neutral.png",
+ "text": "А еще мне сильно хотелось спать...",
+ "durationMs": 2000,
+ "background": "resources/test_cutscene001.png"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/resources/start_dorm.lua b/resources/start_dorm.lua
index d7f2a4e..2d59039 100644
--- a/resources/start_dorm.lua
+++ b/resources/start_dorm.lua
@@ -25,7 +25,7 @@ journal_picked_up = false
function on_npc_interact(npc_index)
if npc_index == 2 then
- local player_alik_aware = game_api.getIntValue("player_alik_aware")
+ local player_alik_aware = game_api.getIntValue("alik_aware")
local player_container_aware = game_api.getIntValue("player_container_aware")
if player_container_aware == 1 then
game_api.start_dialogue("dialog_alik003")
@@ -198,6 +198,37 @@ game_api.set_enter_night_callback(
end
)
+local chat0_opened = false
+local chat1_opened = false
+local chat2_opened = false
+
+game_api.set_chat_callbacks(
+ function()
+ if not chat0_opened then
+ chat0_opened = true
+ game_api.start_dialogue("dialog_chat_aiperi001")
+ end
+ end,
+ function()
+ if not chat1_opened then
+ chat1_opened = true
+ game_api.start_dialogue("dialog_chat_parents001")
+ end
+ end,
+ function()
+ if not chat2_opened then
+ chat2_opened = true
+ game_api.start_dialogue("dialog_chat_news001")
+ end
+ end
+)
+
+function on_aiperi_dialog_over()
+print("on_aiperi_dialog_over called")
+
+game_api.close_phone()
+end
+
game_api.set_location_callbacks(
function()
print("Enter location dorm")
diff --git a/resources/start_uni_interior.lua b/resources/start_uni_interior.lua
index 84b1231..8a3989b 100644
--- a/resources/start_uni_interior.lua
+++ b/resources/start_uni_interior.lua
@@ -16,7 +16,7 @@ player_left_darklands = false
morning_can_open_door_index = 0
morning_did_open_door_index = 0
-player_ghost_aware = false
+--player_ghost_aware = false
ghost_gone = false
@@ -143,6 +143,7 @@ function on_bookshelf_clicked()
end
function on_npc_interact(npc_index)
+ local player_ghost_aware = game_api.getIntValue("ghost_aware")
print("[Lua] NPC interaction! Index: " .. tostring(npc_index))
if npc_index == 1 then
local day = game_api.getIntValue("day")
@@ -154,6 +155,8 @@ function on_npc_interact(npc_index)
game_api.start_dialogue("knife_dialog001")
end
else
+ game_api.start_dialogue("dialog_aiperi_morning001")
+ --[[
if (player_ghost_aware) then
local player_alik_aware = game_api.getIntValue("player_alik_aware")
if player_alik_aware == 1 then
@@ -164,7 +167,7 @@ function on_npc_interact(npc_index)
end
else
game_api.start_dialogue("aiperi_dialog003")
- end
+ end]]
end
end
if npc_index == 0 then
@@ -190,16 +193,11 @@ function on_npc_interact(npc_index)
end
end
if npc_index == 2 then
- if (player_ghost_aware) then
- local report_card_signed = game_api.getIntValue("report_card_signed")
- if (report_card_signed == 1) then
- game_api.start_dialogue("dialog_with_ghost003")
- else
- game_api.start_dialogue("dialog_with_ghost002")
- end
+ local report_card_signed = game_api.getIntValue("report_card_signed")
+ if (report_card_signed == 1) then
+ game_api.start_dialogue("dialog_with_ghost003")
else
game_api.start_dialogue("dialog_with_ghost001")
- --player_ghost_aware = true
end
end
end
@@ -213,11 +211,12 @@ function on_quest_over()
game_api.quest_set_objective_completed("ghost_release", "ghost_release_show")
end
+--[[
function on_first_ghost_dialog_over()
print("on_first_ghost_dialog_over")
player_ghost_aware = true
game_api.quest_unlock("ghost_lore")
-end
+end]]
function on_darklands_over()
game_api.set_player_hp(10)
@@ -234,7 +233,7 @@ end
function on_library_door_click()
print("on_library_door_click---")
local day = game_api.getIntValue("day")
-
+
if (day == 0) then
if (night_time) then
game_api.start_dialogue("door_night_dialog001")
@@ -244,7 +243,7 @@ function on_library_door_click()
end
end
else
- if (morning_can_open_door_index == 5) then
+ --[[if (morning_can_open_door_index == 5) then
if (morning_did_open_door_index == 0) then
morning_did_open_door_index = 5
game_api.start_dialogue("door_unlock_dialog001")
@@ -256,41 +255,32 @@ function on_library_door_click()
end
else
game_api.start_dialogue("door_dialog001")
- end
+ end]]
- end
-
---[[
- --if (player_left_darklands) then
- if (morning_can_open_door_index == 5) then
- if (morning_did_open_door_index == 0) then
- morning_did_open_door_index = 5
- game_api.start_dialogue("door_unlock_dialog001")
- game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil)
- game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
- end)
- game_api.switch_navigation(12)
+ if (game_api.is_night()) then
+ if (not game.is_dawn() or not (morning_did_open_door_index == 4)) then
+ game_api.start_dialogue("door_dialog001")
end
- elseif (night_time && day == 0) then
- game_api.start_dialogue("door_night_dialog001")
- else
- if (not lection_is_over) then
+ end
+ --[[
+ if (game_api.is_night() and not game.is_dawn()) then
game_api.start_dialogue("door_dialog001")
end
- end]]
+ if (game_api.is_night() and game.is_dawn() and not (morning_did_open_door_index == 4) then
+ game_api.start_dialogue("door_dialog001")
+ end]]
+ end
end
function on_teachers_door_click()
print("on_teachers_door_click---")
-
-
+
+
local day = game_api.getIntValue("day")
print("day is")
print(day)
-
+
if (day == 0) then
-
if (player_hold_key) then
if (not teacher_door_opened) then
teacher_door_opened = true
@@ -304,7 +294,6 @@ function on_teachers_door_click()
game_api.start_dialogue("door_teacher_dialog001")
end
else
-
print("morning_can_open_door_index is")
print(morning_can_open_door_index)
if (morning_can_open_door_index == 3) then
@@ -324,7 +313,7 @@ function on_teachers_door_click()
game_api.start_dialogue("door_dialog001")
end
end
-
+
--[[
if (player_left_darklands) then
@@ -356,9 +345,9 @@ end
function on_hall_door_click()
print("on_hall_door_click---")
-
+
local day = game_api.getIntValue("day")
-
+
if (day == 0) then
if (not hall_door_opened) then
hall_door_opened = true
@@ -369,72 +358,53 @@ function on_hall_door_click()
end)
game_api.set_npc_enabled(0, true)
end
-
else
- if (morning_can_open_door_index == 6) then
- if (morning_did_open_door_index == 0) then
- morning_did_open_door_index = 6
- game_api.start_dialogue("door_unlock_dialog001")
-
- game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
- game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
- end)
- game_api.switch_navigation(13)
+ if (game_api.is_night()) then
+ if not (morning_did_open_door_index == 6) then
+ game_api.start_dialogue("door_dialog001")
end
- else
- game_api.start_dialogue("door_dialog001")
+ --[[
+ if (morning_can_open_door_index == 6) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 6
+ game_api.start_dialogue("door_unlock_dialog001")
+
+ game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(13)
+ end
+ else
+ game_api.start_dialogue("door_dialog001")
+ end]]
end
end
-
- --[[
- if (player_left_darklands) then
- if (morning_can_open_door_index == 6) then
- if (morning_did_open_door_index == 0) then
- morning_did_open_door_index = 6
- game_api.start_dialogue("door_unlock_dialog001")
-
- game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
- game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
- end)
- game_api.switch_navigation(13)
- end
- else
- game_api.start_dialogue("door_dialog001")
- end
- elseif (not hall_door_opened) then
- hall_door_opened = true
- game_api.switch_navigation(1)
- game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
- game_api.fade_object("Room_Cover_Main_Hall_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Main_Hall_001")
- end)
- game_api.set_npc_enabled(0, true)
- end]]
end
function on_s1_door_click()
-
-
-
-
-
--if (player_left_darklands) then
- if (morning_can_open_door_index == 1) then
- if (morning_did_open_door_index == 0) then
- morning_did_open_door_index = 1
- game_api.start_dialogue("door_unlock_dialog001")
+ if (game_api.is_night()) then
+ if not (morning_did_open_door_index == 1) then
+ game_api.start_dialogue("door_dialog001")
+ end
+ end
+ --[[
+ if (morning_can_open_door_index == 1) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 1
+ game_api.start_dialogue("door_unlock_dialog001")
- game_api.rotate_object("Room_S_0_Leaf001", 90, 0.5, nil)
- game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
- end)
- game_api.switch_navigation(8)
- end
- else
- game_api.start_dialogue("door_dialog001")
+ game_api.rotate_object("Room_S_0_Leaf001", 90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(8)
end
+ else
+ game_api.start_dialogue("door_dialog001")
+ end
+ ]]
--else
-- game_api.start_dialogue("door_dialog001")
--end
@@ -442,52 +412,62 @@ end
function on_s2_door_click()
--if (player_left_darklands) then
- if (morning_can_open_door_index == 2) then
- if (morning_did_open_door_index == 0) then
- morning_did_open_door_index = 2
- game_api.start_dialogue("door_unlock_dialog001")
+
+ if (game_api.is_night()) then
+ if not (morning_did_open_door_index == 2) then
+ game_api.start_dialogue("door_dialog001")
+ end
+ end
+
+ --[[
+ if (morning_can_open_door_index == 2) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 2
+ game_api.start_dialogue("door_unlock_dialog001")
- game_api.rotate_object("Room_S_1_Leaf001", 90, 0.5, nil)
- game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
- end)
- game_api.switch_navigation(9)
- end
- else
- game_api.start_dialogue("door_dialog001")
+ game_api.rotate_object("Room_S_1_Leaf001", 90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(9)
end
- --else
- -- game_api.start_dialogue("door_dialog001")
- --end
+ else
+ game_api.start_dialogue("door_dialog001")
+ end
+]]
end
function on_n2_door_click()
+
+
print("player_left_darklands")
print(player_left_darklands)
print("morning_can_open_door_index")
print(morning_can_open_door_index)
print("morning_did_open_door_index")
print(morning_did_open_door_index)
- --if (player_left_darklands) then
- if (morning_can_open_door_index == 4) then
- if (morning_did_open_door_index == 0) then
- morning_did_open_door_index = 4
- game_api.start_dialogue("door_unlock_dialog001")
+
+ if (game_api.is_night()) then
+ if not (morning_did_open_door_index == 4) then
+ game_api.start_dialogue("door_dialog001")
+ end
+ end
+
+--[[
+ if (morning_can_open_door_index == 4) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 4
+ game_api.start_dialogue("door_unlock_dialog001")
- game_api.rotate_object("Room_N_1_Leaf001", -90, 0.5, nil)
- game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
- game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
- end)
- game_api.switch_navigation(11)
- else
- --game_api.start_dialogue("door_opened_dialog001")
- end
- else
- game_api.start_dialogue("door_dialog001")
+ game_api.rotate_object("Room_N_1_Leaf001", -90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(11)
end
- --else
- -- game_api.start_dialogue("door_dialog001")
- --end
+ else
+ game_api.start_dialogue("door_dialog001")
+ end]]
end
function on_teacher_arrived()
@@ -605,6 +585,7 @@ function on_note_pickup()
end
function on_report_card_pickup()
+ local player_ghost_aware = game_api.getIntValue("ghost_aware")
if player_ghost_aware then
local player_has_report_card = game_api.getIntValue("player_has_report_card")
local report_card_signed = game_api.getIntValue("report_card_signed")
@@ -848,7 +829,7 @@ end
function setDay1NightSetup()
night_time = true
-
+
print("setDay1NightSetup")
morning_did_open_door_index = 0
@@ -934,7 +915,136 @@ game_api.set_enter_night_callback(
)
+game_api.set_chat_callbacks(
+ function()
+ if (game_api.is_night()) then
+ if (game_api.is_dawn()) then
+ if not (morning_can_open_door_index == 0) and not (morning_can_open_door_index==3) then
+ game_api.start_dialogue("phone_morning_aiperi001")
+ else
+ --TODO: add aiperi that she already came to the uni
+ end
+ else
+ local asked_help_locked = game_api.getIntValue("asked_help_locked")
+ if not (asked_help_locked == 1) then
+ game_api.start_dialogue("phone_night_aiperi001")
+ end
+ end
+ end
+ end,
+ nil,
+ nil
+)
+--[[function on_aiperi_give_keys()
+game_api.pickup_item("all_keys")
+end]]
+
+function on_aiperi_opens_door()
+ print("on_aiperi_opens_door")
+ game_api.close_phone()
+ if (morning_can_open_door_index == 6) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 6
+ game_api.start_dialogue("door_unlock_dialog001")
+
+ game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(13)
+
+ --Aiperi incoming
+ game_api.npc_set_position(1, 0, 0, 6.5)
+ game_api.set_npc_enabled(1, true)
+ game_api.npc_walk_to(1, 0.0, 0, 9.1, function()
+ game_api.start_dialogue("dialog_aiperi_morning001")
+ end)
+ game_api.player_walk_to(0.0, 0.0, 10.0, nil)
+ end
+ elseif (morning_can_open_door_index == 5) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 5
+ game_api.start_dialogue("door_unlock_dialog001")
+ game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(12)
+
+ --Aiperi incoming
+ game_api.npc_set_position(1, 0.0, 0, 6.43818)
+ game_api.set_npc_enabled(1, true)
+ game_api.npc_walk_to(1, 2.34349, 0, 6.31229, function()
+ game_api.start_dialogue("dialog_aiperi_morning001")
+ end)
+ game_api.player_walk_to(3.52174, 0, 6.28993, nil)
+ end
+ elseif (morning_can_open_door_index == 4) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 4
+ game_api.start_dialogue("door_unlock_dialog001")
+
+ game_api.rotate_object("Room_N_1_Leaf001", -90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(11)
+
+ --Aiperi incoming
+ game_api.npc_set_position(1, 0, 0, -1.7911)
+ game_api.set_npc_enabled(1, true)
+ game_api.npc_walk_to(1, 2.11898, 0, -1.7074, function()
+ game_api.start_dialogue("dialog_aiperi_morning001")
+ end)
+ game_api.player_walk_to(3.36055, 0, -2.0345, nil)
+ end
+ elseif (morning_can_open_door_index == 2) then
+
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 2
+ game_api.start_dialogue("door_unlock_dialog001")
+
+ game_api.rotate_object("Room_S_1_Leaf001", 90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(9)
+ --Aiperi incoming
+ game_api.npc_set_position(1, 0, 0, -1.7911)
+ game_api.set_npc_enabled(1, true)
+ game_api.npc_walk_to(1, -2.454, 0, -1.61871, function()
+ game_api.start_dialogue("dialog_aiperi_morning001")
+ end)
+ game_api.player_walk_to(-3.63363, 0, -1.50727, nil)
+ end
+
+
+ elseif (morning_can_open_door_index == 1) then
+ if (morning_did_open_door_index == 0) then
+ morning_did_open_door_index = 1
+ game_api.start_dialogue("door_unlock_dialog001")
+
+ game_api.rotate_object("Room_S_0_Leaf001", 90, 0.5, nil)
+ game_api.fade_object("Room_Cover_Corridor_001", 0, 0.5, function()
+ game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
+ end)
+ game_api.switch_navigation(8)
+
+ --Aiperi incoming
+ game_api.npc_set_position(1, 0, 0, -9.77948)
+ game_api.set_npc_enabled(1, true)
+ game_api.npc_walk_to(1, -2.53977, 0, -9.77852, function()
+ game_api.start_dialogue("dialog_aiperi_morning001")
+ end)
+ game_api.player_walk_to(-3.64494, 0, -9.66711, nil)
+ end
+ end
+end
+
+function on_aiperi_morning_dialog_finished()
+ game_api.npc_walk_to(1, 0.764049, 0, -8.5, nil)
+end
game_api.set_location_callbacks(
function()
diff --git a/src/Game.cpp b/src/Game.cpp
index 5302db7..f031742 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -200,7 +200,7 @@ namespace ZL
LocationSetup uniInteriorParams;
uniInteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_interior.json";
uniInteriorParams.npcsJsonPath = "resources/config2/npcs_uni_interior.json";
- uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues.json";
+ uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues_003.json";
/*
@@ -242,6 +242,7 @@ namespace ZL
};
*/
+
uniInteriorParams.navigationJsonPaths = {
"resources/navigation/uni_interior3_all_locked.json",
"resources/navigation/uni_interior3_hall.json",
@@ -258,6 +259,23 @@ namespace ZL
"resources/navigation/uni_interior3_unlocked_n3.json",
"resources/navigation/uni_interior3_unlocked_hall.json"
};
+ /*
+ uniInteriorParams.navigationJsonPaths = {
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json", //6
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json",
+ "resources/navigation/uni_interior3_darklands_all_open.json"
+ };*/
uniInteriorParams.teleportsJsonPath = "resources/config2/teleports_uni_interior.json";
uniInteriorParams.triggerZonesJsonPath = "resources/config2/trigger_zones_uni_interior.json";
@@ -273,6 +291,7 @@ namespace ZL
locations["uni_interior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { this->menuManager.isNight = isNight; this->menuManager.isDawn = isDawn; };
locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
locations["uni_interior"]->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
+ locations["uni_interior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
if (locations["uni_interior"]->player)
locations["uni_interior"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
for (auto& npc : locations["uni_interior"]->npcs) {
@@ -301,6 +320,7 @@ namespace ZL
locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&globalFloats);
locations["uni_exterior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { this->menuManager.isNight = isNight; this->menuManager.isDawn = isDawn; };
locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
+ locations["uni_exterior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
if (locations["uni_exterior"]->player)
locations["uni_exterior"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
@@ -362,6 +382,7 @@ namespace ZL
locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&globalFloats);
locations["location_dorm"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { this->menuManager.isNight = isNight; this->menuManager.isDawn = isDawn; };
locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
+ locations["location_dorm"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
if (locations["location_dorm"]->player)
locations["location_dorm"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
@@ -391,6 +412,12 @@ namespace ZL
locations["uni_interior"]->onTeleport = teleportCallback;
locations["location_dorm"]->onTeleport = teleportCallback;
+ // Share the global int store with all dialogue runtimes so flags set via
+ // dialogue JSON and via Lua set_dialogue_flag all see the same state.
+ for (auto& [name, loc] : locations) {
+ loc->dialogueSystem.setGlobalFlagStore(&globalInts);
+ }
+
// Wire tutorial advance callbacks for all locations.
// advanceTutorialStep() guards against double-advancing, so sharing is safe.
for (auto& [name, loc] : locations) {
@@ -423,6 +450,11 @@ namespace ZL
startNightTransition();
};
+ menuManager.chatOpenCallback = [this](int chatIndex) {
+ if (currentLocation)
+ currentLocation->scriptEngine.callChatOpenCallback(chatIndex);
+ };
+
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
for (auto& [name, loc] : locations) {
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
diff --git a/src/Location.h b/src/Location.h
index 3be3715..a1b4a9e 100644
--- a/src/Location.h
+++ b/src/Location.h
@@ -116,6 +116,8 @@ namespace ZL
// from step12 to step13 (trigger-zone encounter hint).
std::function requestAdvanceDarklandsHud;
+ std::function requestClosePhone;
+
// Navigation editor — toggle with 'N', save with 'B', right-click to finalize polygon
EditorMode editorMode = EditorMode::None;
LocationEditor editor;
diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp
index c37b5dd..dc6add2 100644
--- a/src/MenuManager.cpp
+++ b/src/MenuManager.cpp
@@ -347,15 +347,15 @@ namespace ZL {
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
chatUnread_[0] = false;
- openPhoneChatFromList(0, phoneChat1Root, "dialog_chat_aiperi001");
+ openPhoneChatFromList(0, phoneChat1Root);
});
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
chatUnread_[1] = false;
- openPhoneChatFromList(1, phoneChat2Root, "dialog_chat_parents001");
+ openPhoneChatFromList(1, phoneChat2Root);
});
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
chatUnread_[2] = false;
- openPhoneChatFromList(2, phoneChat3Root, "dialog_chat_news001");
+ openPhoneChatFromList(2, phoneChat3Root);
});
}
@@ -436,13 +436,11 @@ namespace ZL {
});
}
- void MenuManager::openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot, const std::string& dialogueId) {
+ void MenuManager::openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot) {
activeChatIndex_ = chatIndex;
phoneChatVisibleBubbles_.clear();
uiManager.pushMenuFromSavedRoot(chatRoot);
- const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end();
-
rebuildChatBubblesFromHistory(chatIndex);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
@@ -453,9 +451,8 @@ namespace ZL {
returnToPhoneChatList();
});
- if (firstOpen && startDialogueFunc && !dialogueId.empty()) {
- startedDialogues_.insert(dialogueId);
- startDialogueFunc(dialogueId);
+ if (chatOpenCallback) {
+ chatOpenCallback(chatIndex);
}
}
diff --git a/src/MenuManager.h b/src/MenuManager.h
index 5e54e46..82acd44 100644
--- a/src/MenuManager.h
+++ b/src/MenuManager.h
@@ -8,7 +8,6 @@
#include
#include
#include
-#include
namespace ZL {
@@ -69,6 +68,7 @@ namespace ZL {
std::function startDialogueFunc;
std::function startDarklandsTransitionFunc;
std::function startNightTransitionFunc;
+ std::function chatOpenCallback;
// Called when a chat message bubble should be shown (text + direction)
void onChatBubbleReady(const std::string& text, bool incoming);
@@ -104,7 +104,7 @@ namespace ZL {
void refreshChatUnreadIndicators();
void resetPhoneChatNodes();
void recomputePhoneChatPositions();
- void openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot, const std::string& dialogueId);
+ void openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot);
void returnToPhoneChatList();
void closePhoneScreenFromChat();
void applyUniIntHud();
@@ -166,9 +166,6 @@ namespace ZL {
int selectedQuestIndex = -1;
std::vector visibleQuestIds;
- // Dialogues that have been started at least once; re-opening a chat won't restart them
- std::unordered_set startedDialogues_;
-
bool chatUnread_[3] = { true, true, true };
int money_ = 5500;
diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp
index 8a7a163..f126084 100644
--- a/src/ScriptEngine.cpp
+++ b/src/ScriptEngine.cpp
@@ -26,6 +26,7 @@ namespace ZL {
sol::protected_function darklandsEnterCallback;
sol::protected_function darklandsExitCallback;
sol::protected_function triggerNightEnterCallback;
+ sol::protected_function chatOpenCallbacks[3];
};
ScriptEngine::ScriptEngine() = default;
@@ -65,6 +66,44 @@ namespace ZL {
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
});
+ api.set_function("player_walk_to",
+ [game](float x, float y, float z, sol::object on_arrived) {
+
+ std::function cb;
+ if (on_arrived.is()) {
+ sol::protected_function fn = on_arrived.as();
+ cb = [fn]() mutable {
+ auto result = fn();
+ if (!result.valid()) {
+ sol::error err = result;
+ std::cerr << "[script] on_arrived error: " << err.what() << "\n";
+ }
+ };
+ }
+ game->player->homePosition = Eigen::Vector3f(x, 0.f, z);
+ game->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
+ });
+
+
+ api.set_function("has_item", [inventory](const std::string& itemId) {
+ const Item* item = ItemRegistry::instance().findById(itemId);
+ if (item) {
+ if (inventory->hasItem(itemId)) {
+ std::cout << "[script] has_item: " << item->name << " returns true" << std::endl;
+ return true;
+ }
+ else
+ {
+ std::cout << "[script] has_item: " << item->name << " returns false" << std::endl;
+ return false;
+ }
+ }
+ else {
+ std::cerr << "[script] has_item: item '" << itemId << "' not found in ItemRegistry\n";
+ return false;
+ }
+ });
+
// pickup_item(item_id)
api.set_function("pickup_item", [inventory](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
@@ -229,6 +268,11 @@ namespace ZL {
return game->isNight;
});
+ api.set_function("is_dawn",
+ [game]() {
+ return game->isDawn;
+ });
+
// advance_darklands_hud()
// Advances the uni_interior darklands HUD from step12 to step13.
// Call when the player enters the ghost trigger zone in darklands.
@@ -319,6 +363,19 @@ namespace ZL {
this_impl->darklandsExitCallback = onExit.as();
});
+ api.set_function("close_phone",
+ [game]() {
+ if (game->requestClosePhone)
+ game->requestClosePhone();
+ });
+
+ api.set_function("set_chat_callbacks",
+ [this_impl = impl.get()](sol::object cb0, sol::object cb1, sol::object cb2) {
+ if (cb0.is()) this_impl->chatOpenCallbacks[0] = cb0.as();
+ if (cb1.is()) this_impl->chatOpenCallbacks[1] = cb1.as();
+ if (cb2.is()) this_impl->chatOpenCallbacks[2] = cb2.as();
+ });
+
api.set_function("set_enter_night_callback",
[this_impl = impl.get()](sol::object onEnter) {
if (onEnter.is())
@@ -357,6 +414,7 @@ namespace ZL {
}
Eigen::Vector3f pos(x, y, z);
npcs[index]->position = pos;
+ npcs[index]->homePosition = pos;
npcs[index]->setTarget(pos);
});
@@ -790,6 +848,18 @@ namespace ZL {
}
}
+ void ScriptEngine::callChatOpenCallback(int chatIndex) {
+ if (!impl) return;
+ if (chatIndex < 0 || chatIndex > 2) return;
+ auto& fn = impl->chatOpenCallbacks[chatIndex];
+ if (!fn.valid()) return;
+ auto result = fn();
+ if (!result.valid()) {
+ sol::error err = result;
+ std::cerr << "[SCRIPT] chat open callback error for chat " << chatIndex << ": " << err.what() << "\n";
+ }
+ }
+
void ScriptEngine::callNpcBumpsPlayerCallback(int npcIndex) {
if (!impl) return;
auto it = impl->npcBumpsPlayerCallbacks.find(npcIndex);
diff --git a/src/ScriptEngine.h b/src/ScriptEngine.h
index 7008127..750489b 100644
--- a/src/ScriptEngine.h
+++ b/src/ScriptEngine.h
@@ -48,6 +48,8 @@ public:
void callCutsceneCompleteCallback(const std::string& cutsceneId);
+ void callChatOpenCallback(int chatIndex);
+
private:
struct Impl;
std::unique_ptr impl;
diff --git a/src/dialogue/DialogueRuntime.cpp b/src/dialogue/DialogueRuntime.cpp
index ddf4e6b..0d54af9 100644
--- a/src/dialogue/DialogueRuntime.cpp
+++ b/src/dialogue/DialogueRuntime.cpp
@@ -307,12 +307,17 @@ void DialogueRuntime::skipCurrentCutscene() {
}
void DialogueRuntime::setFlag(const std::string& name, int value) {
- flags[name] = value;
+ if (flagStore) (*flagStore)[name] = value;
}
int DialogueRuntime::getFlag(const std::string& name) const {
- auto it = flags.find(name);
- return (it != flags.end()) ? it->second : 0;
+ if (!flagStore) return 0;
+ auto it = flagStore->find(name);
+ return (it != flagStore->end()) ? it->second : 0;
+}
+
+void DialogueRuntime::setGlobalFlagStore(std::unordered_map* store) {
+ flagStore = store;
}
void DialogueRuntime::setQuestJournal(Quest::QuestJournal* journal) {
@@ -368,10 +373,10 @@ void DialogueRuntime::applyEffects(const std::vector& effects) {
continue;
}
if (effect.relative) {
- flags[effect.flag] += effect.value;
+ setFlag(effect.flag, getFlag(effect.flag) + effect.value);
}
else {
- flags[effect.flag] = effect.value;
+ setFlag(effect.flag, effect.value);
}
}
}
@@ -424,6 +429,8 @@ bool DialogueRuntime::enterNode(const std::string& nodeId) {
return true;
case NodeType::End:
+ if (!node.luaCallback.empty() && onDialogueLineStarted)
+ onDialogueLineStarted(node.luaCallback);
stop();
return true;
@@ -907,7 +914,7 @@ int DialogueRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition
}
return total;
}
-
+/*
DialogueRuntime::json DialogueRuntime::buildSaveState() const {
json result;
result["active"] = isActive();
@@ -917,7 +924,6 @@ DialogueRuntime::json DialogueRuntime::buildSaveState() const {
result["selectedChoice"] = selectedChoice;
result["currentCutsceneLine"] = currentCutsceneLine;
result["cutsceneTimerMs"] = cutsceneTimerMs;
- result["flags"] = flags;
result["consumedChoices"] = consumedChoices;
return result;
}
@@ -930,10 +936,7 @@ bool DialogueRuntime::restoreSaveState(const json& state) {
flags.clear();
consumedChoices.clear();
- if (state.contains("flags")) {
- flags = state["flags"].get>();
- }
- if (state.contains("consumedChoices")) {
+if (state.contains("consumedChoices")) {
consumedChoices = state["consumedChoices"].get>();
}
@@ -963,5 +966,5 @@ bool DialogueRuntime::restoreSaveState(const json& state) {
}
return ok;
}
-
+*/
} // namespace ZL::Dialogue
diff --git a/src/dialogue/DialogueRuntime.h b/src/dialogue/DialogueRuntime.h
index 908db16..b24388a 100644
--- a/src/dialogue/DialogueRuntime.h
+++ b/src/dialogue/DialogueRuntime.h
@@ -43,10 +43,12 @@ public:
void setFlag(const std::string& name, int value);
int getFlag(const std::string& name) const;
+ void setGlobalFlagStore(std::unordered_map* store);
+
void setQuestJournal(Quest::QuestJournal* journal);
- json buildSaveState() const;
- bool restoreSaveState(const json& state);
+ //json buildSaveState() const;
+ //bool restoreSaveState(const json& state);
private:
enum class Mode {
@@ -69,7 +71,7 @@ private:
const DialogueDefinition* activeDialogue = nullptr;
const StaticCutsceneDefinition* activeCutscene = nullptr;
- std::unordered_map flags;
+ std::unordered_map* flagStore = nullptr;
std::unordered_set consumedChoices;
std::string currentNodeId;
diff --git a/src/dialogue/DialogueSystem.h b/src/dialogue/DialogueSystem.h
index 4f8ec8e..28e5f15 100644
--- a/src/dialogue/DialogueSystem.h
+++ b/src/dialogue/DialogueSystem.h
@@ -38,6 +38,8 @@ public:
void setFlag(const std::string& name, int value) { runtime.setFlag(name, value); }
int getFlag(const std::string& name) const { return runtime.getFlag(name); }
+ void setGlobalFlagStore(std::unordered_map* store) { runtime.setGlobalFlagStore(store); }
+
void setQuestJournal(Quest::QuestJournal* journal) { runtime.setQuestJournal(journal); }
private: