Working on translations for dialogs and cutscenes

This commit is contained in:
Vladislav Khorev 2026-07-05 21:52:37 +03:00
parent a98f71676d
commit 794a820be9
25 changed files with 3015 additions and 123 deletions

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
dialogue_translations_from_xlsx.py - Import a translated XLSX spreadsheet back
into dialogue_translations.json.
Reads a spreadsheet with a "key" header column followed by one column per
language (as produced by dialogue_translations_to_xlsx.py, possibly edited by a
translator), and writes it back out as {"dialogue_translations": [{"key":...,
"<lang>":...}, ...]}. Rows with an empty key are skipped. Cell values are
stripped; missing/empty cells become "".
Usage:
python dialogue_translations_from_xlsx.py <input.xlsx> <translations.json>
"""
import json
import os
import sys
try:
from openpyxl import load_workbook
except ImportError:
raise SystemExit("openpyxl is required: pip install openpyxl")
def import_from_xlsx(xlsx_path: str, json_path: str) -> None:
wb = load_workbook(xlsx_path, read_only=True, data_only=True)
ws = wb.active
rows = ws.iter_rows(values_only=True)
header = next(rows, None)
if not header or header[0] != "key":
raise SystemExit(f"Expected first column header to be 'key', got: {header}")
languages = [str(col) for col in header[1:] if col is not None]
entries = []
for row in rows:
key = row[0]
if key is None or str(key).strip() == "":
continue
entry = {"key": str(key)}
for i, lang in enumerate(languages):
value = row[i + 1] if i + 1 < len(row) else None
entry[lang] = str(value).strip() if value is not None else ""
entries.append(entry)
os.makedirs(os.path.dirname(os.path.abspath(json_path)), exist_ok=True)
with open(json_path, 'w', encoding='utf-8') as f:
json.dump({"dialogue_translations": entries}, f, indent=4, ensure_ascii=False)
print(f"Saved: {json_path} ({len(entries)} row(s), languages: {', '.join(languages)})")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.xlsx> <translations.json>")
sys.exit(1)
import_from_xlsx(sys.argv[1], sys.argv[2])

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""
dialogue_translations_to_xlsx.py - Export dialogue_translations.json to an XLSX
spreadsheet for translators.
Reads a translation file shaped as {"dialogue_translations": [{"key":..., "ru":...,
"en":...}, ...]}, and writes one row per entry with a "key" column followed by one
column per language found in the file (order: ru, en, then any others found,
alphabetically). Missing values are written as empty cells.
Usage:
python dialogue_translations_to_xlsx.py <translations.json> <output.xlsx>
"""
import json
import sys
try:
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
except ImportError:
raise SystemExit("openpyxl is required: pip install openpyxl")
PREFERRED_LANGUAGE_ORDER = ("ru", "en")
def collect_languages(entries: list[dict]) -> list[str]:
found = set()
for entry in entries:
found.update(k for k in entry.keys() if k != "key")
ordered = [lang for lang in PREFERRED_LANGUAGE_ORDER if lang in found]
ordered += sorted(found - set(ordered))
return ordered
def export_to_xlsx(json_path: str, xlsx_path: str) -> None:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
entries = data.get("dialogue_translations", [])
languages = collect_languages(entries)
wb = Workbook()
ws = wb.active
ws.title = "dialogue_translations"
ws.append(["key"] + languages)
for entry in entries:
ws.append([entry.get("key", "")] + [entry.get(lang, "") for lang in languages])
ws.freeze_panes = "A2"
ws.column_dimensions["A"].width = 50
for col_index in range(len(languages)):
ws.column_dimensions[get_column_letter(col_index + 2)].width = 50
wb.save(xlsx_path)
print(f"Saved: {xlsx_path} ({len(entries)} row(s), languages: {', '.join(languages)})")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <translations.json> <output.xlsx>")
sys.exit(1)
export_to_xlsx(sys.argv[1], sys.argv[2])

View File

@ -69,6 +69,8 @@ set(SOURCES
../src/CharacterState.h ../src/CharacterState.h
../src/Environment.cpp ../src/Environment.cpp
../src/Environment.h ../src/Environment.h
../src/Localization.cpp
../src/Localization.h
../src/render/Renderer.cpp ../src/render/Renderer.cpp
../src/render/Renderer.h ../src/render/Renderer.h
../src/render/ShaderManager.cpp ../src/render/ShaderManager.cpp
@ -133,6 +135,8 @@ set(SOURCES
../src/dialogue/DialogueOverlay.cpp ../src/dialogue/DialogueOverlay.cpp
../src/dialogue/DialogueSystem.h ../src/dialogue/DialogueSystem.h
../src/dialogue/DialogueSystem.cpp ../src/dialogue/DialogueSystem.cpp
../src/dialogue/TranslationDatabase.h
../src/dialogue/TranslationDatabase.cpp
../src/quest/QuestTypes.h ../src/quest/QuestTypes.h
../src/quest/QuestJournal.h ../src/quest/QuestJournal.h
../src/quest/QuestJournal.cpp ../src/quest/QuestJournal.cpp

View File

@ -20,6 +20,8 @@ add_executable(witcher001
../src/CharacterState.h ../src/CharacterState.h
../src/Environment.cpp ../src/Environment.cpp
../src/Environment.h ../src/Environment.h
../src/Localization.cpp
../src/Localization.h
../src/render/Renderer.cpp ../src/render/Renderer.cpp
../src/render/Renderer.h ../src/render/Renderer.h
../src/render/ShaderManager.cpp ../src/render/ShaderManager.cpp
@ -84,6 +86,8 @@ add_executable(witcher001
../src/dialogue/DialogueOverlay.cpp ../src/dialogue/DialogueOverlay.cpp
../src/dialogue/DialogueSystem.h ../src/dialogue/DialogueSystem.h
../src/dialogue/DialogueSystem.cpp ../src/dialogue/DialogueSystem.cpp
../src/dialogue/TranslationDatabase.h
../src/dialogue/TranslationDatabase.cpp
../src/quest/QuestTypes.h ../src/quest/QuestTypes.h
../src/quest/QuestJournal.h ../src/quest/QuestJournal.h
../src/quest/QuestJournal.cpp ../src/quest/QuestJournal.cpp

View File

@ -92,7 +92,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековной? Я тогда еще приносила скатерть, тарелки и серебряный нож для торта. И я до сих пор не получила назад ничего.",
"next": "line_2", "next": "line_2",
"chatBubble": "in" "chatBubble": "in"
}, },

View File

@ -0,0 +1,134 @@
{
"dialogue_translations": [
{
"key": "Аида Джаныбековна",
"ru": "Аида Джаныбековна",
"en": "Aida Dzhanybekovna"
},
{
"key": "Опаздывающие, заходите скорее и занимайте свои места! Лекция начинается!",
"ru": "Опаздывающие, заходите скорее и занимайте свои места! Лекция начинается!",
"en": "Latecomers, come in quickly and take your seats! The lecture is about to begin!"
},
{
"key": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
"ru": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
"en": "In the episode \"Semetey\" of the trilogy \"Manas\", the traitors Kanchoro and Kyyaz seize power over the Kyrgyz."
},
{
"key": "Чтобы спасти раненого богатыря Семетея, фея Кёкмончок с помощью заклинания уводит его в иной мир.",
"ru": "Чтобы спасти раненого богатыря Семетея, фея Кёкмончок с помощью заклинания уводит его в иной мир.",
"en": "To save the wounded hero Semetey, the fairy Kyokmonchok uses a spell to take him to another world."
},
{
"key": "Этот мир описан в эпосе Манас как Кайып или Аль-Гайб, но некоторые ученые называют его миром теней.",
"ru": "Этот мир описан в эпосе Манас как Кайып или Аль-Гайб, но некоторые ученые называют его миром теней.",
"en": "This world is described in the Manas epic as Kayip or Al-Ghayb, but some scholars call it the dark lands."
},
{
"key": "В этом мире обитают феи, духи и джинны. Простым смертным в этот мир дорога закрыта.",
"ru": "В этом мире обитают феи, духи и джинны. Простым смертным в этот мир дорога закрыта.",
"en": "This world is inhabited by fairies, spirits, and genies. Mere mortals are barred from entering."
},
{
"key": "Время там течет по другому - за один день в теневом мире могут пройти годы жизни обычного мира.",
"ru": "Время там течет по другому - за один день в теневом мире могут пройти годы жизни обычного мира.",
"en": "Time flows differently there - in one day in the dark lands, years of life in the ordinary world can pass."
},
{
"key": "Обычно, мир теней никак не пересекается с нашим миром живых людей.",
"ru": "Обычно, мир теней никак не пересекается с нашим миром живых людей.",
"en": "Usually, the world of dark lands does not intersect with our world of living people."
},
{
"key": "Но в критические моменты для народа, обитатели теневого мира могут приходить в наш мир.",
"ru": "Но в критические моменты для народа, обитатели теневого мира могут приходить в наш мир.",
"en": "But in critical moments for the people, the inhabitants of the dark lands can come to our world."
},
{
"key": "Совсем недавно закончилась пандемия, а сегодня мир захлестнули кровавые войны.",
"ru": "Совсем недавно закончилась пандемия, а сегодня мир захлестнули кровавые войны.",
"en": "The pandemic just ended, and today the world is engulfed in bloody wars."
},
{
"key": "В такие кризисные моменты истории, грань между мирами становится особенно тонкой.",
"ru": "В такие кризисные моменты истории, грань между мирами становится особенно тонкой.",
"en": "At such critical moments in history, the line between worlds becomes especially thin."
},
{
"key": "На сегодня лекция завершена. Все свободны! Задания на этот модуль вы получите индивидуально!",
"ru": "На сегодня лекция завершена. Все свободны! Задания на этот модуль вы получите индивидуально!",
"en": "This lecture is over for today. Everyone is free to go! You will receive assignments for this module individually!"
},
{
"key": "Я сел за компьютер и начал писать эссе по книге",
"ru": "Я сел за компьютер и начал писать эссе по книге",
"en": "I sat down at the computer and started writing an essay on the book."
},
{
"key": "Книга была очень скучной. Как будто кто-то наложил на книгу чары сна.",
"ru": "Книга была очень скучной. Как будто кто-то наложил на книгу чары сна.",
"en": "The book was very boring. It was as if someone had cast a sleeping spell on it."
},
{
"key": "Неудивительно что я стал понемногу засыпать...",
"ru": "Неудивительно что я стал понемногу засыпать...",
"en": "It's no wonder that I started to fall asleep little by little..."
},
{
"key": "Мгновенно как я упал без сил, что-то сверкнуло.",
"ru": "Мгновенно как я упал без сил, что-то сверкнуло.",
"en": "The moment I fell down without strength, something flashed."
},
{
"key": "Я очнулся и понял, что я по-прежнему в универе.",
"ru": "Я очнулся и понял, что я по-прежнему в универе.",
"en": "I woke up and realized that I was still at university."
},
{
"key": "Все тело болело, как будто я всю ночь таскал мешки с цементом.",
"ru": "Все тело болело, как будто я всю ночь таскал мешки с цементом.",
"en": "My whole body ached, as if I had been carrying bags of cement all night."
},
{
"key": "А еще мне сильно хотелось спать...",
"ru": "А еще мне сильно хотелось спать...",
"en": "And I also really wanted to sleep..."
},
{
"key": "Я лег спать и уснул.",
"ru": "Я лег спать и уснул.",
"en": "I went to bed and fell asleep."
},
{
"key": "Долгожданный сон полностью восстановил мои силы.",
"ru": "Долгожданный сон полностью восстановил мои силы.",
"en": "The long-awaited sleep completely restored my strength."
},
{
"key": "Я проснулся на следующий день уже после обеда.",
"ru": "Я проснулся на следующий день уже после обеда.",
"en": "I woke up the next day after lunch."
},
{
"key": "Все призраки исчезли, вокруг стало тихо.",
"ru": "Все призраки исчезли, вокруг стало тихо.",
"en": "All the ghosts disappeared and it became quiet around."
},
{
"key": "Кажется, у меня получилось изгнать призрака.",
"ru": "Кажется, у меня получилось изгнать призрака.",
"en": "I think I've exorcised the ghost."
},
{
"key": "Теперь надо добавить этот пункт к себе в резюме.",
"ru": "Теперь надо добавить этот пункт к себе в резюме.",
"en": "Now I need to add this item to my resume."
},
{
"key": "В тот момент я еще не знал, что мои приключения еще только начинаются...",
"ru": "В тот момент я еще не знал, что мои приключения еще только начинаются...",
"en": "At that moment, I didn't yet know that my adventures were just beginning..."
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -271,7 +271,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековной? Я тогда еще приносила скатерть, тарелки и серебряный нож для торта. И я до сих пор не получила назад ничего.",
"next": "line_2", "next": "line_2",
"chatBubble": "in" "chatBubble": "in"
}, },

View File

@ -156,7 +156,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png", "portrait": "resources/dialogue/portrait_aiperi.png",
"text": "Помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", "text": "Помнишь мы скидывались на торт для Аиды Джаныбековной? Я тогда еще приносила скатерть, тарелки и серебряный нож для торта. И я до сих пор не получила назад ничего.",
"next": "line_7" "next": "line_7"
}, },
{ {
@ -212,7 +212,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png", "portrait": "resources/dialogue/portrait_aiperi.png",
"text": "А во вторых, мне как-то не хочется попадаться на глаза Аиде. Я даже лекцию пропустила.", "text": "А во вторых, мне как-то не хочется попадаться на глаза Аиде.",
"next": "line_14" "next": "line_14"
}, },
{ {
@ -236,7 +236,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png", "portrait": "resources/dialogue/portrait_aiperi.png",
"text": "Да... Только не говори Аиде Джаныбековой что я здесь.", "text": "Да... Только не говори Аиде Джаныбековной что я здесь.",
"next": "line_18" "next": "line_18"
}, },
{ {
@ -268,7 +268,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png", "portrait": "resources/dialogue/portrait_aiperi.png",
"text": "Постарайся только Аиде Джаныбековой особо на глаза не попадаться. ", "text": "Постарайся только Аиде Джаныбековной особо на глаза не попадаться. ",
"next": "line_23" "next": "line_23"
}, },
{ {
@ -527,7 +527,7 @@
{ {
"id": "line_8", "id": "line_8",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Зачем тебе в учительскую?", "text": "Зачем тебе в учительскую?",
"next": "line_9" "next": "line_9"
@ -543,15 +543,15 @@
{ {
"id": "line_10", "id": "line_10",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Это разве твой нож? Этот нож мне одолжила Айпери.", "text": "Это разве твой нож? Этот нож принадлежит Айпери.",
"next": "line_11" "next": "line_11"
}, },
{ {
"id": "line_11", "id": "line_11",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Пусть она сама лично и приходит.", "text": "Пусть она сама лично и приходит.",
"next": "line_12" "next": "line_12"
@ -559,7 +559,7 @@
{ {
"id": "line_12", "id": "line_12",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "А ты даже не смей заходить в учительскую без моего разрешения!", "text": "А ты даже не смей заходить в учительскую без моего разрешения!",
"next": "condition_3" "next": "condition_3"
@ -569,7 +569,7 @@
"type": "Line", "type": "Line",
"speaker": "Бекзат", "speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png", "portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Хорошо....", "text": "Хорошо...",
"next": "line_5" "next": "line_5"
}, },
{ {
@ -596,7 +596,7 @@
{ {
"id": "line_3", "id": "line_3",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Тогда не отвлекай меня, я занята!", "text": "Тогда не отвлекай меня, я занята!",
"next": "condition_2" "next": "condition_2"
@ -617,7 +617,7 @@
{ {
"id": "line_5", "id": "line_5",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Кстати, если встретишь Айпери, передай ей, чтобы она подошла ко мне.", "text": "Кстати, если встретишь Айпери, передай ей, чтобы она подошла ко мне.",
"next": "line_6" "next": "line_6"
@ -625,9 +625,9 @@
{ {
"id": "line_6", "id": "line_6",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я ей отдам нож, и заодно я дам ей новоен задание на модуль.", "text": "Я ей отдам нож, и заодно я дам ей новое задание на модуль.",
"next": "setflag_1" "next": "setflag_1"
}, },
{ {
@ -646,7 +646,7 @@
"type": "Line", "type": "Line",
"speaker": "Бекзат", "speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png", "portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Хорошо, извините, я пойду....", "text": "Хорошо, извините, я пойду...",
"next": "end_1" "next": "end_1"
}, },
{ {
@ -677,7 +677,7 @@
{ {
"id": "line_20", "id": "line_20",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я же сказала, дай мне сначала найти книги в библиотеке.", "text": "Я же сказала, дай мне сначала найти книги в библиотеке.",
"next": "line_21" "next": "line_21"
@ -685,7 +685,7 @@
{ {
"id": "line_21", "id": "line_21",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Когда я закончу с книжными делами, подойдешь ко мне и получишь задание.", "text": "Когда я закончу с книжными делами, подойдешь ко мне и получишь задание.",
"next": "line_4" "next": "line_4"
@ -693,7 +693,7 @@
{ {
"id": "line_15", "id": "line_15",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я тебя сегодня впервые вижу на парах.", "text": "Я тебя сегодня впервые вижу на парах.",
"next": "line_16" "next": "line_16"
@ -709,7 +709,7 @@
{ {
"id": "line_17", "id": "line_17",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Хорошо, я дам тебе какое-нибудь задание, но не сейчас.", "text": "Хорошо, я дам тебе какое-нибудь задание, но не сейчас.",
"next": "line_18" "next": "line_18"
@ -717,7 +717,7 @@
{ {
"id": "line_18", "id": "line_18",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я занята, как видишь, я пытаюсь разобраться в каталоге книг в этой библиотеке.", "text": "Я занята, как видишь, я пытаюсь разобраться в каталоге книг в этой библиотеке.",
"next": "line_19" "next": "line_19"
@ -725,7 +725,7 @@
{ {
"id": "line_19", "id": "line_19",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Когда я закончу, можешь подойти.", "text": "Когда я закончу, можешь подойти.",
"next": "setflag_2" "next": "setflag_2"
@ -759,7 +759,7 @@
{ {
"id": "line_7", "id": "line_7",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Тебя не предупреждали, что без моего разрешения в учительскую заходить нельзя?", "text": "Тебя не предупреждали, что без моего разрешения в учительскую заходить нельзя?",
"next": "line_8" "next": "line_8"
@ -767,7 +767,7 @@
{ {
"id": "line_8", "id": "line_8",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Как ты вообще сюда попал?", "text": "Как ты вообще сюда попал?",
"next": "line_9" "next": "line_9"
@ -783,7 +783,7 @@
{ {
"id": "line_10", "id": "line_10",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Вот лиса! Я же сказала ей, чтобы она лично забрала нож. А она взяла и тебя отправила.", "text": "Вот лиса! Я же сказала ей, чтобы она лично забрала нож. А она взяла и тебя отправила.",
"next": "line_11" "next": "line_11"
@ -791,7 +791,7 @@
{ {
"id": "line_11", "id": "line_11",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ты точно отдашь этот нож Айпери?", "text": "Ты точно отдашь этот нож Айпери?",
"next": "line_21" "next": "line_21"
@ -807,7 +807,7 @@
{ {
"id": "line_22", "id": "line_22",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Хорошо. Когда встретишь ее, отдай нож и передай ей, что я ее жду.", "text": "Хорошо. Когда встретишь ее, отдай нож и передай ей, что я ее жду.",
"next": "line_12" "next": "line_12"
@ -823,7 +823,7 @@
{ {
"id": "line_13", "id": "line_13",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Нет, Бекзат. Ты вломился в учительскую, и теперь хочешь просто так уйти? Не получится.", "text": "Нет, Бекзат. Ты вломился в учительскую, и теперь хочешь просто так уйти? Не получится.",
"next": "line_14" "next": "line_14"
@ -831,7 +831,7 @@
{ {
"id": "line_14", "id": "line_14",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я придумала для тебя особенное задание на этот модуль.", "text": "Я придумала для тебя особенное задание на этот модуль.",
"next": "line_2" "next": "line_2"
@ -847,7 +847,7 @@
{ {
"id": "line_15", "id": "line_15",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ты не сможешь найти ничего по этой теме в интернете. Даже и не пытайся. Тебе придется писать эссе по книге.", "text": "Ты не сможешь найти ничего по этой теме в интернете. Даже и не пытайся. Тебе придется писать эссе по книге.",
"next": "line_3" "next": "line_3"
@ -873,7 +873,7 @@
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбековна", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Там в библиотеке есть компьютер, напиши презентацию прямо на нем.", "text": "Там в библиотеке есть компьютер, напиши эссе прямо на нем.",
"next": "line_16" "next": "line_16"
}, },
{ {
@ -887,7 +887,7 @@
{ {
"id": "line_17", "id": "line_17",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "До завтра.", "text": "До завтра.",
"next": "line_18" "next": "line_18"
@ -903,7 +903,7 @@
{ {
"id": "line_19", "id": "line_19",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "В твоих интересах успеть. Завтра покажешь мне, что у тебя получилось.", "text": "В твоих интересах успеть. Завтра покажешь мне, что у тебя получилось.",
"next": "line_20" "next": "line_20"
@ -911,7 +911,7 @@
{ {
"id": "line_20", "id": "line_20",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Не сдашь модуль - пойдешь на второй год. В другой раз не будешь трогать вещи в учительской без разрешения.", "text": "Не сдашь модуль - пойдешь на второй год. В другой раз не будешь трогать вещи в учительской без разрешения.",
"next": "line_6" "next": "line_6"
@ -974,7 +974,7 @@
{ {
"id": "line_1", "id": "line_1",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Бекзат, зачетка Бегимай у тебя. Отправляйся в теневой мир и покажи зачетку призраку Бегимай, чтобы она освободилась.", "text": "Бекзат, зачетка Бегимай у тебя. Отправляйся в теневой мир и покажи зачетку призраку Бегимай, чтобы она освободилась.",
"objectiveComplete": "", "objectiveComplete": "",
@ -1046,7 +1046,7 @@
{ {
"id": "line_7", "id": "line_7",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Теперь иди в теневой мир и покажи зачетку Бегимай. Расскажи ей, что она сдала курсовую работу.", "text": "Теперь иди в теневой мир и покажи зачетку Бегимай. Расскажи ей, что она сдала курсовую работу.",
"next": "line_6" "next": "line_6"
@ -1214,7 +1214,7 @@
"type": "Line", "type": "Line",
"speaker": "Бегимай", "speaker": "Бегимай",
"portrait": "resources/dialogue/portrait_ghost.png", "portrait": "resources/dialogue/portrait_ghost.png",
"text": "Я уйду на покой только когда увиду оценку по курсовой в своей зачетке.", "text": "Я уйду на покой только когда увижу оценку по курсовой в своей зачетке.",
"next": "line_7" "next": "line_7"
}, },
{ {
@ -1222,7 +1222,7 @@
"type": "Line", "type": "Line",
"speaker": "Бегимай", "speaker": "Бегимай",
"portrait": "resources/dialogue/portrait_ghost.png", "portrait": "resources/dialogue/portrait_ghost.png",
"text": "А до тех пор, я буду появлятся здесь каждую ночь.", "text": "А до тех пор, я буду появляться здесь каждую ночь.",
"next": "line_8" "next": "line_8"
}, },
{ {
@ -2228,7 +2228,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png", "portrait": "resources/dialogue/portrait_aiperi.png",
"text": "Тебе еще и не такое привидется. Езжай в общагу и выспись.", "text": "Тебе еще и не такое привидится. Езжай в общагу и выспись.",
"next": "setflag_1" "next": "setflag_1"
}, },
{ {
@ -2252,7 +2252,7 @@
"type": "Line", "type": "Line",
"speaker": "Бекзат", "speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png", "portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Аида Джаныбекова увидела меня в учительской, и заставила писать эссе по этой тупой книге.", "text": "Аида Джаныбековна увидела меня в учительской, и заставила писать эссе по этой тупой книге.",
"next": "line_9" "next": "line_9"
}, },
{ {
@ -2508,7 +2508,7 @@
"type": "Line", "type": "Line",
"speaker": "Айпери", "speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековной? Я тогда еще приносила скатерть, тарелки и серебряный нож для торта. И я до сих пор не получила назад ничего.",
"next": "line_2", "next": "line_2",
"chatBubble": "in" "chatBubble": "in"
}, },
@ -3058,7 +3058,7 @@
{ {
"id": "line_1", "id": "line_1",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Бекзат, ты подготовил эссе на тему \"Манас в изложении Жусупа Мамая\"?", "text": "Бекзат, ты подготовил эссе на тему \"Манас в изложении Жусупа Мамая\"?",
"objectiveComplete": "", "objectiveComplete": "",
@ -3075,7 +3075,7 @@
{ {
"id": "line_4", "id": "line_4",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Найди книгу про Жусупа Мамая в библиотеке, а потом садись за компьютер и пиши!", "text": "Найди книгу про Жусупа Мамая в библиотеке, а потом садись за компьютер и пиши!",
"next": "line_3" "next": "line_3"
@ -3123,7 +3123,7 @@
{ {
"id": "line_99", "id": "line_99",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ну что Бекзат, эссе про Жусупа Мамая готово к сдаче?", "text": "Ну что Бекзат, эссе про Жусупа Мамая готово к сдаче?",
"next": "line_2" "next": "line_2"
@ -3133,7 +3133,7 @@
"type": "Line", "type": "Line",
"speaker": "Бекзат", "speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png", "portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Прощу прощения Аида Джаныбековна, я правда старался, но у меня возникли обстоятельства непреодолимой силы.", "text": "Прошу прощения Аида Джаныбековна, я правда старался, но у меня возникли обстоятельства непреодолимой силы.",
"next": "line_3" "next": "line_3"
}, },
{ {
@ -3171,7 +3171,7 @@
{ {
"id": "line_6", "id": "line_6",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Какое еще заклинание? У тебя эта записка с собой?", "text": "Какое еще заклинание? У тебя эта записка с собой?",
"next": "line_7" "next": "line_7"
@ -3187,7 +3187,7 @@
{ {
"id": "line_8", "id": "line_8",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Надеюсь, ты не читал это заклинание?", "text": "Надеюсь, ты не читал это заклинание?",
"next": "line_9" "next": "line_9"
@ -3211,7 +3211,7 @@
{ {
"id": "line_12", "id": "line_12",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Это заклинание переместило тебя в мир теней. ", "text": "Это заклинание переместило тебя в мир теней. ",
"next": "line_13" "next": "line_13"
@ -3219,7 +3219,7 @@
{ {
"id": "line_13", "id": "line_13",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Этот мир предназначен для фей и джиннов. Обычные люди туда попасть не могут. Но у тебя это как-то получилось.", "text": "Этот мир предназначен для фей и джиннов. Обычные люди туда попасть не могут. Но у тебя это как-то получилось.",
"next": "line_14" "next": "line_14"
@ -3235,7 +3235,7 @@
{ {
"id": "line_15", "id": "line_15",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Не забывай, я изучаю фольклор в целом и Манас в частности. Если бы ты не засыпал над книгами, ты бы тоже много интересного узнал про мир от наших предков.", "text": "Не забывай, я изучаю фольклор в целом и Манас в частности. Если бы ты не засыпал над книгами, ты бы тоже много интересного узнал про мир от наших предков.",
"next": "line_16" "next": "line_16"
@ -3243,7 +3243,7 @@
{ {
"id": "line_16", "id": "line_16",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я освобождаю тебя от задания по эссе. И взамен я дам тебе другое задание. Если хочешь сдать модуль, тебе придется выполнить его.", "text": "Я освобождаю тебя от задания по эссе. И взамен я дам тебе другое задание. Если хочешь сдать модуль, тебе придется выполнить его.",
"next": "line_17" "next": "line_17"
@ -3259,7 +3259,7 @@
{ {
"id": "line_18", "id": "line_18",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "В универе завелся призрак. Конечно, его не получится увидеть вживую.", "text": "В универе завелся призрак. Конечно, его не получится увидеть вживую.",
"next": "line_19" "next": "line_19"
@ -3267,7 +3267,7 @@
{ {
"id": "line_19", "id": "line_19",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Но иногда по вечерам я слышу плач из лекционного зала.", "text": "Но иногда по вечерам я слышу плач из лекционного зала.",
"next": "line_20" "next": "line_20"
@ -3275,7 +3275,7 @@
{ {
"id": "line_20", "id": "line_20",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "А на утро бывает так, что все зачетки в шкафу раскиданы как попало.", "text": "А на утро бывает так, что все зачетки в шкафу раскиданы как попало.",
"next": "line_21" "next": "line_21"
@ -3283,7 +3283,7 @@
{ {
"id": "line_21", "id": "line_21",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Призрак обитает где-то в теневом мире. Попади туда и наладь с ним диалог. Узнай, кто он и что ему нужно.", "text": "Призрак обитает где-то в теневом мире. Попади туда и наладь с ним диалог. Узнай, кто он и что ему нужно.",
"next": "line_22" "next": "line_22"
@ -3291,7 +3291,7 @@
{ {
"id": "line_22", "id": "line_22",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Если ты успешно избавишься от призрака, я поставлю тебе максимальный балл за модуль.", "text": "Если ты успешно избавишься от призрака, я поставлю тебе максимальный балл за модуль.",
"next": "setflag_4" "next": "setflag_4"
@ -3310,7 +3310,7 @@
{ {
"id": "line_23", "id": "line_23",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "У тебя есть еще вопросы, Бекзат?", "text": "У тебя есть еще вопросы, Бекзат?",
"next": "choice_1" "next": "choice_1"
@ -3359,7 +3359,7 @@
{ {
"id": "line_25", "id": "line_25",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Теневой мир это мир, в котором обитают феи, духи и джинны. Он существовал всегда.", "text": "Теневой мир это мир, в котором обитают феи, духи и джинны. Он существовал всегда.",
"next": "line_26" "next": "line_26"
@ -3367,7 +3367,7 @@
{ {
"id": "line_26", "id": "line_26",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Обычные люди, не воители, туда не попадают обычно. Кроме тебя, тебе это как-то удалось.", "text": "Обычные люди, не воители, туда не попадают обычно. Кроме тебя, тебе это как-то удалось.",
"next": "line_31" "next": "line_31"
@ -3375,7 +3375,7 @@
{ {
"id": "line_31", "id": "line_31",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Спрашивай что хочешь, я попробую объяснить.", "text": "Спрашивай что хочешь, я попробую объяснить.",
"next": "choice_2" "next": "choice_2"
@ -3430,7 +3430,7 @@
{ {
"id": "line_30", "id": "line_30",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я думаю, в мир теней проецируются только фундаментальные объекты, типа зданий, деревьев, рельефа.", "text": "Я думаю, в мир теней проецируются только фундаментальные объекты, типа зданий, деревьев, рельефа.",
"next": "line_27" "next": "line_27"
@ -3438,7 +3438,7 @@
{ {
"id": "line_27", "id": "line_27",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Это происходит потому, что в теневом мире время течет сильно медленнее, чем в нашем мире.", "text": "Это происходит потому, что в теневом мире время течет сильно медленнее, чем в нашем мире.",
"next": "line_32" "next": "line_32"
@ -3446,7 +3446,7 @@
{ {
"id": "line_32", "id": "line_32",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Двери у нас слишком часто открываются и закрываются, поэтому в теневом мире они \"размываются\" или попадают в суперпозицию.", "text": "Двери у нас слишком часто открываются и закрываются, поэтому в теневом мире они \"размываются\" или попадают в суперпозицию.",
"next": "line_28" "next": "line_28"
@ -3454,7 +3454,7 @@
{ {
"id": "line_28", "id": "line_28",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Поэтому в мире теней ты можешь спокойно пройти через дверной проем. Но сквозь стены ты пройти не сможешь, они более плотные.", "text": "Поэтому в мире теней ты можешь спокойно пройти через дверной проем. Но сквозь стены ты пройти не сможешь, они более плотные.",
"next": "line_49" "next": "line_49"
@ -3474,7 +3474,7 @@
{ {
"id": "line_34", "id": "line_34",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Это их мир, и они там живут. Я думаю что увидев тебя, обитатели теневого мира тоже были шокированы.", "text": "Это их мир, и они там живут. Я думаю что увидев тебя, обитатели теневого мира тоже были шокированы.",
"next": "line_35" "next": "line_35"
@ -3482,7 +3482,7 @@
{ {
"id": "line_35", "id": "line_35",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Но кое в чем ты прав. Здесь в универе, призраков раньше не было. Всякая чертовщина начала происходить совсем недавно.", "text": "Но кое в чем ты прав. Здесь в универе, призраков раньше не было. Всякая чертовщина начала происходить совсем недавно.",
"next": "line_36" "next": "line_36"
@ -3490,7 +3490,7 @@
{ {
"id": "line_36", "id": "line_36",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ты должен узнать, откуда здесь появился призрак, и заставить его исчезнуть.", "text": "Ты должен узнать, откуда здесь появился призрак, и заставить его исчезнуть.",
"next": "line_37" "next": "line_37"
@ -3498,7 +3498,7 @@
{ {
"id": "line_37", "id": "line_37",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Помимо призрака, есть еще и тени. Но ты не беспокойся, это всего лишь проекции негативных мыслей.", "text": "Помимо призрака, есть еще и тени. Но ты не беспокойся, это всего лишь проекции негативных мыслей.",
"next": "line_38" "next": "line_38"
@ -3506,7 +3506,7 @@
{ {
"id": "line_38", "id": "line_38",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Просто избавляйся от теней, когда встретишь их. А когда ты изгонишь призрака, тени исчезнут сами. ", "text": "Просто избавляйся от теней, когда встретишь их. А когда ты изгонишь призрака, тени исчезнут сами. ",
"next": "line_49" "next": "line_49"
@ -3522,7 +3522,7 @@
{ {
"id": "line_40", "id": "line_40",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Время в мире теней течет значительно медленнее, чем в нашем.", "text": "Время в мире теней течет значительно медленнее, чем в нашем.",
"next": "line_41" "next": "line_41"
@ -3530,7 +3530,7 @@
{ {
"id": "line_41", "id": "line_41",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "И похоже, что течение времени там нелинейное.", "text": "И похоже, что течение времени там нелинейное.",
"next": "line_42" "next": "line_42"
@ -3538,7 +3538,7 @@
{ {
"id": "line_42", "id": "line_42",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Это объясняет, почему в теневом мире есть деревья и строения, но отсутствуют двери, автомобили, и прочее.", "text": "Это объясняет, почему в теневом мире есть деревья и строения, но отсутствуют двери, автомобили, и прочее.",
"next": "line_43" "next": "line_43"
@ -3546,7 +3546,7 @@
{ {
"id": "line_43", "id": "line_43",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Объекты в нашем мире должны долго находится на одном месте, прежде чем их проекция появится в мире теней.", "text": "Объекты в нашем мире должны долго находится на одном месте, прежде чем их проекция появится в мире теней.",
"next": "line_49" "next": "line_49"
@ -3562,7 +3562,7 @@
{ {
"id": "line_45", "id": "line_45",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Если ты потеряешь сознание в теневом мире, то ты принудительно вернешься в наш мир.", "text": "Если ты потеряешь сознание в теневом мире, то ты принудительно вернешься в наш мир.",
"next": "line_46" "next": "line_46"
@ -3570,7 +3570,7 @@
{ {
"id": "line_46", "id": "line_46",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Также избегай света луны, луна тоже может вернуть тебя в мир людей. Лучше вообще не выходи из помещения. ", "text": "Также избегай света луны, луна тоже может вернуть тебя в мир людей. Лучше вообще не выходи из помещения. ",
"next": "line_47" "next": "line_47"
@ -3578,7 +3578,7 @@
{ {
"id": "line_47", "id": "line_47",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Если ты задержишься, и в человеческом мире настанет утро, тебя тоже выбросит из теневого мира.", "text": "Если ты задержишься, и в человеческом мире настанет утро, тебя тоже выбросит из теневого мира.",
"next": "line_48" "next": "line_48"
@ -3586,7 +3586,7 @@
{ {
"id": "line_48", "id": "line_48",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Но ты не беспокойся. Даже если тебя выбросит из теневого мира, ты всегда сможешь повторить свою попытку на следующую ночь.", "text": "Но ты не беспокойся. Даже если тебя выбросит из теневого мира, ты всегда сможешь повторить свою попытку на следующую ночь.",
"next": "line_49" "next": "line_49"
@ -3594,7 +3594,7 @@
{ {
"id": "line_49", "id": "line_49",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "У тебя есть еще вопросы про теневой мир?", "text": "У тебя есть еще вопросы про теневой мир?",
"next": "choice_2" "next": "choice_2"
@ -3610,7 +3610,7 @@
{ {
"id": "line_51", "id": "line_51",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ты хочешь что-нибудь еще обсудить?", "text": "Ты хочешь что-нибудь еще обсудить?",
"next": "choice_1" "next": "choice_1"
@ -3626,7 +3626,7 @@
{ {
"id": "line_53", "id": "line_53",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Нет, это не я. И на самом деле, я немного обеспокоена этим.", "text": "Нет, это не я. И на самом деле, я немного обеспокоена этим.",
"next": "line_56" "next": "line_56"
@ -3634,7 +3634,7 @@
{ {
"id": "line_56", "id": "line_56",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Когда я узнала, что в университете начались паранормальные явления, я сразу поняла что это призрак в теневом мире.", "text": "Когда я узнала, что в университете начались паранормальные явления, я сразу поняла что это призрак в теневом мире.",
"next": "line_54" "next": "line_54"
@ -3642,7 +3642,7 @@
{ {
"id": "line_54", "id": "line_54",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я знаю заклинание, которое тебе дали. У этого заклинания есть еще одно условие - оно сработает только на людей, которые чисты сердцем.", "text": "Я знаю заклинание, которое тебе дали. У этого заклинания есть еще одно условие - оно сработает только на людей, которые чисты сердцем.",
"next": "line_57" "next": "line_57"
@ -3650,7 +3650,7 @@
{ {
"id": "line_57", "id": "line_57",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я взяла серебряный нож Айпери и пробовала прочитать заклинание, и у меня не получилось.", "text": "Я взяла серебряный нож Айпери и пробовала прочитать заклинание, и у меня не получилось.",
"next": "line_55" "next": "line_55"
@ -3658,7 +3658,7 @@
{ {
"id": "line_55", "id": "line_55",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Тогда я предположила, что Айпери уж точно сможет прочитать заклинание и попасть в теневой мир.", "text": "Тогда я предположила, что Айпери уж точно сможет прочитать заклинание и попасть в теневой мир.",
"next": "line_58" "next": "line_58"
@ -3666,7 +3666,7 @@
{ {
"id": "line_58", "id": "line_58",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Как раз поэтому я хотела вернуть нож ей лично. И заодно отправить ее в теневой мир.", "text": "Как раз поэтому я хотела вернуть нож ей лично. И заодно отправить ее в теневой мир.",
"next": "line_59" "next": "line_59"
@ -3674,7 +3674,7 @@
{ {
"id": "line_59", "id": "line_59",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Но ты смешал все мои планы. Сначала ты забрал нож, а потом кто-то подсунул тебе записку.", "text": "Но ты смешал все мои планы. Сначала ты забрал нож, а потом кто-то подсунул тебе записку.",
"next": "line_60" "next": "line_60"
@ -3682,7 +3682,7 @@
{ {
"id": "line_60", "id": "line_60",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Это очень странно. Как будто кто-то играет против меня, и кто-то специально ввел тебя в игру заместо Айпери.", "text": "Это очень странно. Как будто кто-то играет против меня, и кто-то специально ввел тебя в игру заместо Айпери.",
"next": "line_61" "next": "line_61"
@ -3706,7 +3706,7 @@
{ {
"id": "line_63", "id": "line_63",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Узнай что нужно призраку, и постарайся уговорить его покинуть университет.", "text": "Узнай что нужно призраку, и постарайся уговорить его покинуть университет.",
"next": "line_64" "next": "line_64"
@ -3714,7 +3714,7 @@
{ {
"id": "line_64", "id": "line_64",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Или ты хочешь вместо этого писать эссе про Жусупа Мамая?", "text": "Или ты хочешь вместо этого писать эссе про Жусупа Мамая?",
"next": "line_65" "next": "line_65"
@ -3780,7 +3780,7 @@
{ {
"id": "line_72", "id": "line_72",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Я не знаю ни одной студентки по имени Бегимай.", "text": "Я не знаю ни одной студентки по имени Бегимай.",
"next": "line_73" "next": "line_73"
@ -3788,7 +3788,7 @@
{ {
"id": "line_73", "id": "line_73",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Но с другой стороны, я здесь преподаю только с этого года. Возможно, Бегимай училась у предыдущего преподавателя?", "text": "Но с другой стороны, я здесь преподаю только с этого года. Возможно, Бегимай училась у предыдущего преподавателя?",
"next": "line_74" "next": "line_74"
@ -3804,7 +3804,7 @@
{ {
"id": "line_75", "id": "line_75",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Опроси студентов и узнай подробнее, кто эта призрак Бегимай и откуда она взялась.", "text": "Опроси студентов и узнай подробнее, кто эта призрак Бегимай и откуда она взялась.",
"next": "line_76" "next": "line_76"
@ -3812,7 +3812,7 @@
{ {
"id": "line_76", "id": "line_76",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Потом возвращайся ко мне, и мы обсудим, что делать дальше.", "text": "Потом возвращайся ко мне, и мы обсудим, что делать дальше.",
"next": "setflag_1" "next": "setflag_1"
@ -3839,7 +3839,7 @@
{ {
"id": "line_78", "id": "line_78",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "У тебя есть еще вопросы?", "text": "У тебя есть еще вопросы?",
"next": "choice_4" "next": "choice_4"
@ -3884,7 +3884,7 @@
{ {
"id": "line_67", "id": "line_67",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Переходи в теневой мир, а затем иди в лекционный зал.", "text": "Переходи в теневой мир, а затем иди в лекционный зал.",
"next": "line_68" "next": "line_68"
@ -3892,7 +3892,7 @@
{ {
"id": "line_68", "id": "line_68",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Постарайся поговорить с призраком, и узнать, что ему нужно.", "text": "Постарайся поговорить с призраком, и узнать, что ему нужно.",
"next": "line_69" "next": "line_69"
@ -3900,7 +3900,7 @@
{ {
"id": "line_69", "id": "line_69",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Потом возвращайся ко мне, и мы обсудим, как можно его изгнать.", "text": "Потом возвращайся ко мне, и мы обсудим, как можно его изгнать.",
"next": "line_77" "next": "line_77"
@ -3963,7 +3963,7 @@
{ {
"id": "line_95", "id": "line_95",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Найди курсовую работу Бегимай и принеси мне ее. Я проверю курсовую вместо предыдущего преподавателя и выставлю ей оценку.", "text": "Найди курсовую работу Бегимай и принеси мне ее. Я проверю курсовую вместо предыдущего преподавателя и выставлю ей оценку.",
"next": "line_77" "next": "line_77"
@ -3984,7 +3984,7 @@
{ {
"id": "line_97", "id": "line_97",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Иди к шкафу в учительской. Среди зачетных книжек найти зачетку Бегимай и принеси мне. Я поставлю ей оценку за курсовую работу.", "text": "Иди к шкафу в учительской. Среди зачетных книжек найти зачетку Бегимай и принеси мне. Я поставлю ей оценку за курсовую работу.",
"next": "line_77" "next": "line_77"
@ -3992,7 +3992,7 @@
{ {
"id": "line_96", "id": "line_96",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ты уже нашел курсовую работу Бегимай?", "text": "Ты уже нашел курсовую работу Бегимай?",
"next": "line_92" "next": "line_92"
@ -4040,7 +4040,7 @@
{ {
"id": "line_87", "id": "line_87",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Печальная история.", "text": "Печальная история.",
"next": "line_88" "next": "line_88"
@ -4048,7 +4048,7 @@
{ {
"id": "line_88", "id": "line_88",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Судя по всему, чтобы освободить ее, нужно найти ее курсовую работу и поставить ей оценку.", "text": "Судя по всему, чтобы освободить ее, нужно найти ее курсовую работу и поставить ей оценку.",
"next": "line_89" "next": "line_89"
@ -4056,7 +4056,7 @@
{ {
"id": "line_89", "id": "line_89",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Ты нашел ее курсовую работу?", "text": "Ты нашел ее курсовую работу?",
"next": "condition_4" "next": "condition_4"
@ -4085,7 +4085,7 @@
{ {
"id": "line_93", "id": "line_93",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Да, вижу. Курсовая работа написана очень хорошо. Пожалуй, я могу выставить за нее максимальный балл.", "text": "Да, вижу. Курсовая работа написана очень хорошо. Пожалуй, я могу выставить за нее максимальный балл.",
"next": "line_98" "next": "line_98"
@ -4093,7 +4093,7 @@
{ {
"id": "line_98", "id": "line_98",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Теперь поищи в шкафу зачетку Бегимай, и принеси мне, я выставлю ей оценку.", "text": "Теперь поищи в шкафу зачетку Бегимай, и принеси мне, я выставлю ей оценку.",
"next": "setflag_3" "next": "setflag_3"
@ -4120,7 +4120,7 @@
{ {
"id": "line_91", "id": "line_91",
"type": "Line", "type": "Line",
"speaker": "Аида Джаныбекова", "speaker": "Аида Джаныбековна",
"portrait": "resources/dialogue/portrait_teacher.png", "portrait": "resources/dialogue/portrait_teacher.png",
"text": "Найди и принеси мне ее. Я проверю работу за предыдущего преподавателя и выставлю ей оценку.", "text": "Найди и принеси мне ее. Я проверю работу за предыдущего преподавателя и выставлю ей оценку.",
"next": "setflag_2" "next": "setflag_2"

View File

@ -1,5 +1,6 @@
#include "Game.h" #include "Game.h"
#include "AnimatedModel.h" #include "AnimatedModel.h"
#include "Localization.h"
#include "utils/Utils.h" #include "utils/Utils.h"
#include <fstream> #include <fstream>
#include "items/ItemRegistry.h" #include "items/ItemRegistry.h"
@ -150,6 +151,11 @@ namespace ZL
{ {
std::cout << "Load resurces step 12x1" << std::endl; std::cout << "Load resurces step 12x1" << std::endl;
// Must happen before createLocations() below: some location scripts
// (e.g. resources/start_dorm.lua) start a dialogue as soon as they're
// loaded, so the language needs to be known before that first load.
ZL::loadLanguageSetting();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);

38
src/Localization.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "Localization.h"
#include "external/nlohmann/json.hpp"
#include "utils/Utils.h"
#include <iostream>
namespace ZL {
Language g_currentLanguage = Language::Russian;
std::string languageToCode(Language lang) {
switch (lang) {
case Language::English: return "en";
case Language::Russian:
default: return "ru";
}
}
Language languageFromCode(const std::string& code) {
if (code == "en") return Language::English;
return Language::Russian;
}
void loadLanguageSetting() {
const std::string content = readTextFile("settings.json");
if (content.empty()) return;
try {
const nlohmann::json root = nlohmann::json::parse(content);
if (root.contains("language")) {
g_currentLanguage = languageFromCode(root["language"].get<std::string>());
}
} catch (const std::exception& e) {
std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl;
}
}
} // namespace ZL

23
src/Localization.h Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <string>
namespace ZL {
enum class Language {
Russian,
English
};
// Currently selected language for in-game text. Defaults to Russian.
extern Language g_currentLanguage;
std::string languageToCode(Language lang);
Language languageFromCode(const std::string& code);
// Reads the "language" field from settings.json (if present) and updates
// g_currentLanguage. Safe to call even if settings.json doesn't exist yet.
// Must run before any location/dialogue content is loaded, since some
// location scripts start dialogues eagerly as soon as they're loaded.
void loadLanguageSetting();
} // namespace ZL

View File

@ -164,12 +164,13 @@ namespace ZL
std::cout << "Shadow map initialized" << std::endl; std::cout << "Shadow map initialized" << std::endl;
//#endif //#endif
setupNavigation(params.navigationJsonPaths); setupNavigation(params.navigationJsonPaths);
dialogueSystem.init(renderer, CONST_ZIP_FILE); dialogueSystem.init(renderer, CONST_ZIP_FILE);
dialogueSystem.loadDatabase(params.dialoguesJsonPath); dialogueSystem.loadDatabase(params.dialoguesJsonPath);
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes008.json"); dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes008.json");
dialogueSystem.loadTranslations("resources/dialogue/translate_dialogues.json");
dialogueSystem.loadCutsceneTranslations("resources/dialogue/translate_cutscenes.json");
dialogueSystem.setQuestJournal(journal); dialogueSystem.setQuestJournal(journal);
npcNameText = std::make_unique<TextRenderer>(); npcNameText = std::make_unique<TextRenderer>();

View File

@ -1,5 +1,6 @@
#include "MenuManager.h" #include "MenuManager.h"
#include "Game.h" #include "Game.h"
#include "Localization.h"
#include "render/TextRenderer.h" #include "render/TextRenderer.h"
#include "utils/Utils.h" #include "utils/Utils.h"
#include <iostream> #include <iostream>
@ -412,10 +413,10 @@ namespace ZL {
}); });
uiManager.setTextButtonCallback("languageRussianButton", [this](const std::string&) { uiManager.setTextButtonCallback("languageRussianButton", [this](const std::string&) {
ZL::g_currentLanguage = ZL::Language::Russian;
}); });
uiManager.setTextButtonCallback("languageEnglishButton", [this](const std::string&) { uiManager.setTextButtonCallback("languageEnglishButton", [this](const std::string&) {
ZL::g_currentLanguage = ZL::Language::English;
}); });
} }
@ -1821,6 +1822,7 @@ namespace ZL {
root["musicEnabled"] = audioPlayer_.isMusicEnabled(); root["musicEnabled"] = audioPlayer_.isMusicEnabled();
root["soundEnabled"] = audioPlayer_.isSoundEnabled(); root["soundEnabled"] = audioPlayer_.isSoundEnabled();
root["fullscreen"] = Environment::isFullscreen; root["fullscreen"] = Environment::isFullscreen;
root["language"] = ZL::languageToCode(ZL::g_currentLanguage);
std::ofstream file("settings.json"); std::ofstream file("settings.json");
if (file.is_open()) { if (file.is_open()) {
@ -1835,6 +1837,11 @@ namespace ZL {
// directly in the right fullscreen state (see WIN32 main() in main.cpp) to avoid // directly in the right fullscreen state (see WIN32 main() in main.cpp) to avoid
// a windowed -> fullscreen flash. The block below just keeps Environment::isFullscreen // a windowed -> fullscreen flash. The block below just keeps Environment::isFullscreen
// in sync in case this is ever called from a path that didn't pre-apply it. // in sync in case this is ever called from a path that didn't pre-apply it.
// Language is loaded separately, even earlier (see ZL::loadLanguageSetting()
// callers), because some location scripts start a dialogue as soon as they're
// loaded, before this function ever runs.
ZL::loadLanguageSetting();
const std::string content = ZL::readTextFile("settings.json"); const std::string content = ZL::readTextFile("settings.json");
if (content.empty()) return; if (content.empty()) return;
try { try {

View File

@ -10,6 +10,14 @@ void CutsceneRuntime::setDatabase(const CutsceneDatabase* value) {
database = value; database = value;
} }
void CutsceneRuntime::setTranslationDatabase(const ZL::Dialogue::TranslationDatabase* value) {
translations = value;
}
std::string CutsceneRuntime::tr(const std::string& s) const {
return translations ? translations->translate(s, ZL::g_currentLanguage) : s;
}
void CutsceneRuntime::setOnFinished(std::function<void(const std::string&)> cb) { void CutsceneRuntime::setOnFinished(std::function<void(const std::string&)> cb) {
onFinished = std::move(cb); onFinished = std::move(cb);
} }
@ -329,9 +337,9 @@ void CutsceneRuntime::refreshPresentation() {
} }
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
presentation.speaker = line.speaker; presentation.speaker = tr(line.speaker);
presentation.fullText = line.text; presentation.fullText = tr(line.text);
presentation.visibleText = line.text; presentation.visibleText = presentation.fullText;
presentation.selectedChoice = 0; presentation.selectedChoice = 0;
} }

View File

@ -2,6 +2,8 @@
#include "cutscene/CutsceneDatabase.h" #include "cutscene/CutsceneDatabase.h"
#include "dialogue/DialogueTypes.h" #include "dialogue/DialogueTypes.h"
#include "dialogue/TranslationDatabase.h"
#include "Localization.h"
#include <functional> #include <functional>
#include <string> #include <string>
@ -10,6 +12,7 @@ namespace ZL::Cutscene {
class CutsceneRuntime { class CutsceneRuntime {
public: public:
void setDatabase(const CutsceneDatabase* value); void setDatabase(const CutsceneDatabase* value);
void setTranslationDatabase(const ZL::Dialogue::TranslationDatabase* value);
void setOnFinished(std::function<void(const std::string&)> cb); void setOnFinished(std::function<void(const std::string&)> cb);
void setOnLineStarted(std::function<void(const std::string&)> cb); void setOnLineStarted(std::function<void(const std::string&)> cb);
@ -27,6 +30,7 @@ public:
private: private:
const CutsceneDatabase* database = nullptr; const CutsceneDatabase* database = nullptr;
const ZL::Dialogue::TranslationDatabase* translations = nullptr;
const StaticCutsceneDefinition* activeCutscene = nullptr; const StaticCutsceneDefinition* activeCutscene = nullptr;
std::string activeCutsceneId; std::string activeCutsceneId;
@ -50,6 +54,7 @@ private:
void syncLineToElapsedTime(); void syncLineToElapsedTime();
void advanceLine(); void advanceLine();
void refreshPresentation(); void refreshPresentation();
std::string tr(const std::string& s) const;
std::vector<PresentedCutsceneImage> evaluateImages() const; std::vector<PresentedCutsceneImage> evaluateImages() const;

View File

@ -15,6 +15,14 @@ void DialogueRuntime::setDatabase(const DialogueDatabase* value) {
database = value; database = value;
} }
void DialogueRuntime::setTranslationDatabase(const TranslationDatabase* value) {
translations = value;
}
std::string DialogueRuntime::tr(const std::string& s) const {
return translations ? translations->translate(s, g_currentLanguage) : s;
}
bool DialogueRuntime::startDialogue(const std::string& dialogueId) { bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
if (!database) { if (!database) {
std::cerr << "[dialogue] No database assigned to runtime\n"; std::cerr << "[dialogue] No database assigned to runtime\n";
@ -295,14 +303,14 @@ void DialogueRuntime::presentLine(const Node& node) {
mode = Mode::PresentingLine; mode = Mode::PresentingLine;
revealCharacters = 0.0f; revealCharacters = 0.0f;
presentation.mode = PresentationMode::Dialogue; presentation.mode = PresentationMode::Dialogue;
presentation.speaker = node.speaker; presentation.speaker = tr(node.speaker);
presentation.fullText = node.text; presentation.fullText = tr(node.text);
presentation.visibleText.clear(); presentation.visibleText.clear();
presentation.portraitPath = node.portrait; presentation.portraitPath = node.portrait;
presentation.choices.clear(); presentation.choices.clear();
presentation.selectedChoice = -1; presentation.selectedChoice = -1;
presentation.revealCompleted = node.text.empty(); presentation.revealCompleted = presentation.fullText.empty();
presentation.showCutsceneSubtitle = false; presentation.showCutsceneSubtitle = false;
presentation.cutsceneSkippable = false; presentation.cutsceneSkippable = false;
@ -311,8 +319,8 @@ void DialogueRuntime::presentLine(const Node& node) {
presentation.cutsceneBlackAlpha = 0.0f; presentation.cutsceneBlackAlpha = 0.0f;
if (presentation.revealCompleted) { if (presentation.revealCompleted) {
presentation.visibleText = node.text; presentation.visibleText = presentation.fullText;
revealCharacters = static_cast<float>(node.text.size()); revealCharacters = static_cast<float>(presentation.fullText.size());
} }
applyQuestActions(node.questUnlock, node.questComplete, node.questFail, applyQuestActions(node.questUnlock, node.questComplete, node.questFail,
@ -321,7 +329,7 @@ void DialogueRuntime::presentLine(const Node& node) {
onDialogueLineStarted(node.luaCallback); onDialogueLineStarted(node.luaCallback);
} }
if (!node.chatBubble.empty() && onChatBubbleReady) { if (!node.chatBubble.empty() && onChatBubbleReady) {
onChatBubbleReady(node.text, node.chatBubble == "in"); onChatBubbleReady(presentation.fullText, node.chatBubble == "in");
} }
} }
@ -333,7 +341,7 @@ void DialogueRuntime::presentChoices(const Node& node) {
if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) continue; if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) continue;
if (!evaluateConditions(choice.conditions)) continue; if (!evaluateConditions(choice.conditions)) continue;
visibleChoices.push_back(choice); visibleChoices.push_back(choice);
presentation.choices.push_back({ choice.id, choice.text, choice.kind }); presentation.choices.push_back({ choice.id, tr(choice.text), choice.kind });
} }
if (visibleChoices.empty()) { if (visibleChoices.empty()) {
@ -349,9 +357,9 @@ void DialogueRuntime::presentChoices(const Node& node) {
mode = Mode::WaitingForChoice; mode = Mode::WaitingForChoice;
selectedChoice = -1; selectedChoice = -1;
presentation.mode = PresentationMode::Choice; presentation.mode = PresentationMode::Choice;
presentation.speaker = node.speaker; presentation.speaker = tr(node.speaker);
presentation.fullText = node.text; presentation.fullText = tr(node.text);
presentation.visibleText = node.text; presentation.visibleText = presentation.fullText;
presentation.portraitPath = node.portrait; presentation.portraitPath = node.portrait;
presentation.selectedChoice = -1; presentation.selectedChoice = -1;

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "dialogue/DialogueDatabase.h" #include "dialogue/DialogueDatabase.h"
#include "dialogue/TranslationDatabase.h"
#include "quest/QuestJournal.h" #include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp" #include "external/nlohmann/json.hpp"
#include "ISaveable.h" #include "ISaveable.h"
@ -15,6 +16,7 @@ namespace ZL::Dialogue {
class DialogueRuntime : public ZL::ISaveable { class DialogueRuntime : public ZL::ISaveable {
public: public:
void setDatabase(const DialogueDatabase* value); void setDatabase(const DialogueDatabase* value);
void setTranslationDatabase(const TranslationDatabase* value);
bool startDialogue(const std::string& dialogueId); bool startDialogue(const std::string& dialogueId);
void stop(); void stop();
@ -62,6 +64,7 @@ private:
std::function<void(const std::string&, bool)> onChatBubbleReady; std::function<void(const std::string&, bool)> onChatBubbleReady;
const DialogueDatabase* database = nullptr; const DialogueDatabase* database = nullptr;
const TranslationDatabase* translations = nullptr;
Quest::QuestJournal* questJournal = nullptr; Quest::QuestJournal* questJournal = nullptr;
const DialogueDefinition* activeDialogue = nullptr; const DialogueDefinition* activeDialogue = nullptr;
@ -86,6 +89,7 @@ private:
bool enterNode(const std::string& nodeId); bool enterNode(const std::string& nodeId);
void presentLine(const Node& node); void presentLine(const Node& node);
void presentChoices(const Node& node); void presentChoices(const Node& node);
std::string tr(const std::string& s) const;
}; };
} // namespace ZL::Dialogue } // namespace ZL::Dialogue

View File

@ -4,7 +4,9 @@ namespace ZL::Dialogue {
bool DialogueSystem::init(Renderer& renderer, const std::string& zipFile) { bool DialogueSystem::init(Renderer& renderer, const std::string& zipFile) {
dialogueRuntime.setDatabase(&database); dialogueRuntime.setDatabase(&database);
dialogueRuntime.setTranslationDatabase(&translationDatabase);
cutsceneRuntime.setDatabase(&cutsceneDatabase); cutsceneRuntime.setDatabase(&cutsceneDatabase);
cutsceneRuntime.setTranslationDatabase(&cutsceneTranslationDatabase);
// When dialogue hits a CutsceneStart node, hand off to cutsceneRuntime. // When dialogue hits a CutsceneStart node, hand off to cutsceneRuntime.
dialogueRuntime.setOnCutsceneStartNeeded([this](const std::string& cutsceneId, const std::string& nextNodeId) { dialogueRuntime.setOnCutsceneStartNeeded([this](const std::string& cutsceneId, const std::string& nextNodeId) {
@ -30,6 +32,14 @@ bool DialogueSystem::loadCutsceneDatabase(const std::string& path) {
return cutsceneDatabase.loadFromFile(path); return cutsceneDatabase.loadFromFile(path);
} }
bool DialogueSystem::loadTranslations(const std::string& path) {
return translationDatabase.loadFromFile(path);
}
bool DialogueSystem::loadCutsceneTranslations(const std::string& path) {
return cutsceneTranslationDatabase.loadFromFile(path);
}
void DialogueSystem::update(int deltaMs) { void DialogueSystem::update(int deltaMs) {
dialogueRuntime.update(deltaMs); dialogueRuntime.update(deltaMs);
cutsceneRuntime.update(deltaMs); cutsceneRuntime.update(deltaMs);

View File

@ -20,6 +20,8 @@ public:
bool loadDatabase(const std::string& path); bool loadDatabase(const std::string& path);
bool loadCutsceneDatabase(const std::string& path); bool loadCutsceneDatabase(const std::string& path);
bool loadTranslations(const std::string& path);
bool loadCutsceneTranslations(const std::string& path);
void update(int deltaMs); void update(int deltaMs);
void draw(Renderer& renderer); void draw(Renderer& renderer);
@ -64,10 +66,12 @@ public:
private: private:
DialogueDatabase database; DialogueDatabase database;
TranslationDatabase translationDatabase;
DialogueRuntime dialogueRuntime; DialogueRuntime dialogueRuntime;
DialogueOverlay dialogueOverlay; DialogueOverlay dialogueOverlay;
ZL::Cutscene::CutsceneDatabase cutsceneDatabase; ZL::Cutscene::CutsceneDatabase cutsceneDatabase;
TranslationDatabase cutsceneTranslationDatabase;
ZL::Cutscene::CutsceneRuntime cutsceneRuntime; ZL::Cutscene::CutsceneRuntime cutsceneRuntime;
ZL::Cutscene::CutsceneOverlay cutsceneOverlay; ZL::Cutscene::CutsceneOverlay cutsceneOverlay;

View File

@ -0,0 +1,80 @@
#include "dialogue/TranslationDatabase.h"
#include "utils/Utils.h"
#include <iostream>
namespace ZL
{
extern const char* CONST_ZIP_FILE;
}
namespace ZL::Dialogue {
bool TranslationDatabase::loadFromFile(const std::string& path) {
entries.clear();
std::string raw;
try {
if (strlen(CONST_ZIP_FILE) == 0) {
raw = readTextFile(path);
}
else {
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
if (buf.empty()) {
std::cerr << "[translation] Failed to read " << path << " from zip\n";
return false;
}
raw.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "[translation] Failed to open " << path << ": " << e.what() << "\n";
return false;
}
json root;
try {
root = json::parse(raw);
}
catch (const std::exception& e) {
std::cerr << "[translation] JSON parse error in " << path << ": " << e.what() << "\n";
return false;
}
if (root.contains("dialogue_translations") && root["dialogue_translations"].is_array()) {
for (const auto& item : root["dialogue_translations"]) {
const std::string key = item.value("key", "");
if (key.empty()) continue;
std::unordered_map<std::string, std::string> langMap;
for (auto it = item.begin(); it != item.end(); ++it) {
if (it.key() == "key") continue;
if (it.value().is_string()) {
langMap[it.key()] = it.value().get<std::string>();
}
}
entries[key] = std::move(langMap);
}
}
return !entries.empty();
}
std::string TranslationDatabase::translate(const std::string& key, Language lang) const {
if (key.empty()) return key;
auto it = entries.find(key);
if (it == entries.end()) return key;
const auto& langMap = it->second;
auto langIt = langMap.find(languageToCode(lang));
if (langIt != langMap.end() && !langIt->second.empty()) return langIt->second;
auto ruIt = langMap.find("ru");
if (ruIt != langMap.end() && !ruIt->second.empty()) return ruIt->second;
return key;
}
} // namespace ZL::Dialogue

View File

@ -0,0 +1,26 @@
#pragma once
#include "Localization.h"
#include "external/nlohmann/json.hpp"
#include <string>
#include <unordered_map>
namespace ZL::Dialogue {
// Loads a shared translation file mapping dialogue "key" strings (the literal
// speaker/text/choice text found in dialogue config files) to per-language text.
class TranslationDatabase {
public:
using json = nlohmann::json;
bool loadFromFile(const std::string& path);
// Returns the translated text for `key` in `lang`, falling back to Russian,
// then to `key` itself if no entry or translation exists.
std::string translate(const std::string& key, Language lang) const;
private:
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> entries;
};
} // namespace ZL::Dialogue

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
sync_cutscene_translations.py - Sync a cutscene config's strings into a translation file.
Reads <cutscene_config.json>, collects every non-empty "speaker" and "text" string
from its lines, and appends any string not already present as a "key" in
<translations.json> as a new entry with an empty "en" slot ready for a translator
to fill in. New entries are always appended at the end of the list so missing
translations are easy to locate.
Cutscene translations are intentionally kept in their own file/database, separate
from dialogue translations - point <translations.json> at a dedicated file such as
resources/dialogue/cutscene_translations.json.
Usage:
python sync_cutscene_translations.py <cutscene_config.json> <translations.json>
"""
import json
import sys
from translation_sync_common import sync_translations
def collect_strings(cutscene_config: dict) -> list[str]:
seen = set()
ordered = []
def add(value):
if value and value not in seen:
seen.add(value)
ordered.append(value)
for cutscene in cutscene_config.get("cutscenes", []):
for line in cutscene.get("lines", []):
add(line.get("speaker", ""))
add(line.get("text", ""))
return ordered
def sync(cutscene_path: str, translations_path: str) -> None:
with open(cutscene_path, 'r', encoding='utf-8') as f:
cutscene_config = json.load(f)
sync_translations(collect_strings(cutscene_config), translations_path)
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <cutscene_config.json> <translations.json>")
sys.exit(1)
sync(sys.argv[1], sys.argv[2])

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
sync_dialogue_translations.py - Sync a dialogue config's strings into a translation file.
Reads <dialogue_config.json>, collects every non-empty "speaker" and "text" string
from its nodes (and "text" from any node's "choices"), and appends any string not
already present as a "key" in <translations.json> as a new entry with an empty
"en" slot ready for a translator to fill in. New entries are always appended at
the end of the list so missing translations are easy to locate.
Usage:
python sync_dialogue_translations.py <dialogue_config.json> <translations.json>
"""
import json
import sys
from translation_sync_common import sync_translations
def collect_strings(dialogue_config: dict) -> list[str]:
seen = set()
ordered = []
def add(value):
if value and value not in seen:
seen.add(value)
ordered.append(value)
for dialogue in dialogue_config.get("dialogues", []):
for node in dialogue.get("nodes", []):
add(node.get("speaker", ""))
add(node.get("text", ""))
for choice in node.get("choices", []):
add(choice.get("text", ""))
return ordered
def sync(dialogue_path: str, translations_path: str) -> None:
with open(dialogue_path, 'r', encoding='utf-8') as f:
dialogue_config = json.load(f)
sync_translations(collect_strings(dialogue_config), translations_path)
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <dialogue_config.json> <translations.json>")
sys.exit(1)
sync(sys.argv[1], sys.argv[2])

View File

@ -0,0 +1,38 @@
"""
translation_sync_common.py - Shared merge/write logic for the *_translations sync
scripts (sync_dialogue_translations.py, sync_cutscene_translations.py).
Each sync script collects strings from its own JSON shape (dialogue nodes/choices,
cutscene lines, ...) and hands the ordered list of unique strings to
sync_translations(), which merges them into a translations JSON file shaped as
{"dialogue_translations": [{"key":..., "ru":..., "en":...}, ...]}. New entries are
always appended at the end so missing translations are easy to locate.
"""
import json
import os
def sync_translations(strings: list[str], translations_path: str) -> None:
if os.path.isfile(translations_path):
with open(translations_path, 'r', encoding='utf-8') as f:
translations = json.load(f)
else:
translations = {}
entries = translations.setdefault("dialogue_translations", [])
existing_keys = {entry.get("key") for entry in entries}
added = 0
for text in strings:
if text in existing_keys:
continue
entries.append({"key": text, "ru": text, "en": ""})
existing_keys.add(text)
added += 1
os.makedirs(os.path.dirname(os.path.abspath(translations_path)), exist_ok=True)
with open(translations_path, 'w', encoding='utf-8') as f:
json.dump(translations, f, indent=4, ensure_ascii=False)
print(f"Saved: {translations_path} ({added} new key(s) added, {len(entries)} total)")