Добавлен , опубликован

Диалоги

Содержание:
Вы уже знакомы с базовыми принципами работы диалогов и теперь, открывая файл диалога, должны видеть не только набор кракозябр, но и какую-никакую структуру. В этом уроке мы углубимся в изучение их устройства на примере реального диалога из игры.

Диалог с тавернщиком

Независимо от мода, который вы ковыряете, у вас должен быть файл ..\PROGRAM\DIALOGS\Common_Tavern.c - это диалог с тавернщиком.
Его наполнение может отличаться в деталях, но нас интересует только основное - как строится диалог, как достигается вариативность ответов и какие функции используются.
Разбор этого файла мы разделим на 2 части: основную и квестовую. Сейчас мы сосредоточимся на основной - в данный момент нам важно понять как устроены диалоги в игре, научиться их читать и редактировать.
Разбирать мы будем следующий код:
void ProcessCommonDialogEvent(ref NPChar, aref Link, aref Diag) {
	ref PChar;
	PChar = GetMainCharacter();
	string NPC_Meeting;
	
	switch(Dialog.CurrentNode) {
		case "First time":
			Dialog.defAni = "dialog_stay1";
			Dialog.defCam = "1";
			Dialog.defLinkAni = "dialog_1";
			Dialog.defLinkCam = "1";
			Dialog.defLinkSnd = "dialogs\woman\024";
			Dialog.ani = "dialog_stay2";
			Dialog.cam = "1";
			Diag.TempNode = "First time";

			if(NPChar.quest.meeting == "0") {
				dialog.snd = "Voice\CHWI\CHWI001";
				Dialog.Text = "Добро пожаловать, " + GetAddress_Form(NPChar) + ". Еда, выпивка и развлечения - все по умеренным ценам!";
				Link.l1 = "Посмотрим... Я " + GetFullName(pchar) + ".";
				Link.l1.go = "meeting";
				NPChar.quest.meeting = "1";
				Dialog.snd = "dialogs\speech\chrar001";
			} else {
				dialog.snd1 = "Voice\CHWI\CHWI002";
				dialog.snd2 = "Voice\CHWI\CHWI003";
				dialog.snd3 = "Voice\CHWI\CHWI004";
				Dialog.Text = RandPhrase("Эй, " + GetAddress_Form(NPChar) + " " + PChar.name + "! " + TimeGreeting() + ".",
                                    "О, какие у нас гости! Рад видеть вас, " + GetAddress_Form(NPChar) + " " + PChar.name + ".",
                                    "Хорошо, что вы заглянули ко мне, " + PChar.name + ". Каков будет сегодня ваш заказ?", &dialog, dialog.snd1, dialog.snd2, dialog.snd3);
				Link.l1 = "Есть на острове парни, готовые уйти в плавание?";
				Link.l1.go = "crew hire";
				Link.l2 = "Я надеюсь ты сможешь ответить на пару вопросов.";
				Link.l2.go = "quest lines";
				link.l3 = pcharrepphrase("Есть ли в твоем клоповнике свободная конура?", "Нет ли у тебя свободной комнаты, я хотел"+ GetSexPhrase("","а") +" бы остановиться здесь на некоторое время.");
				link.l3.go = "room";
				Link.l4 = "Увы, я уже ухожу, " + NPChar.name + ". До встречи.";
				Link.l4.go = "exit";
			}
		break;

        case "exit":
			Diag.CurrentNode = Diag.TempNode;
			NPChar.quest.meeting = NPC_Meeting;
			DialogExit();
		break;

		case "crew hire":
			if (sti(Pchar.Ship.Type) == SHIP_NOTUSED || Pchar.location.from_sea != NPChar.from_sea) {
                if (NPChar.from_sea != "") {
					Dialog.text = "А на что тебе матросы? Что-то не вижу твоего корабля в порту.";
					link.l1 = RandPhraseSimple("Точно... я его пришвартовал"+ GetSexPhrase("","а") +" не в том месте", "Забыл"+ GetSexPhrase("","а") +" войти в порт...");
					link.l1.go = "exit";
					break;
				}
			}
			
            if (makeint(environment.time) > 22.0 || makeint(environment.time) < 10.0) {
				dialog.snd = "Voice\CHWI\CHWI011";
				Dialog.text = "Обычно у меня в таверне полно людей, желающих стать матросами, но сейчас слишком поздно, и они начнут появляться только утром. Может быть, вы хотите снять комнату, и подождать их?";
				link.l1 = "Хорошо. У тебя есть свободные комнаты?";
				link.l1.go = "room";
				link.l2 = "Меня не интересует комната на данный момент. Давай сменим тему.";
				link.l2.go = "no quest";
			} else {
                if (GetNationRelation2MainCharacter(sti(NPChar.nation)) == RELATION_ENEMY && sti(NPChar.nation) != PIRATE) {
                    Dialog.text = "А ты думаешь к тебе кто-то пойдет, когда ты с нами во вражде? Скажи спасибо, что стражу не зову.";
					link.l1 = "Спасибо.";
					link.l1.go = "exit";
                } else {
					Diag.CurrentNode = Diag.TempNode;
					NPChar.quest.meeting = NPC_Meeting;
					DialogExit();
					LaunchHireCrew();
				}
			}
		break;
    }
}
Начало нам уже знакомо, за исключением того, что в обработчик явно передаются ссылки, которые мы ранее объявляли в начале: ProcessCommonDialogEvent(ref NPChar, aref Link, aref Diag). Связано это с тем, что данный диалог - общий для нескольких персонажей. Внимательный читатель мог обратить внимание на слово Common в названии метода. Да, это другой метод и вызывается он немного иначе, но эта тема выходит за рамки данного урока.
Далее мы создаём алиас для главного героя и из нового лишь переменная NPC_Meeting - о её роли мы ещё поговорим. Следом идёт конструкция switch и открывает её уже знакомый нам кейс "First time". С него и начнём:
case "First time":
    Dialog.defAni = "dialog_stay1";
    Dialog.defCam = "1";
    Dialog.defLinkAni = "dialog_1";
    Dialog.defLinkCam = "1";
    Dialog.defLinkSnd = "dialogs\woman\024";
    Dialog.ani = "dialog_stay2";
    Dialog.cam = "1";
    Diag.TempNode = "First time";

    // ...
break;
В самом начале мы видим остатки былой роскоши от Пиратов Карибского Моря - эти функции задают анимацию портрета персонажа и воспроизведение озвучки, которой никогда небыло ни в предыдущих, ни в последующих частях и модификациях.
Поэтому, если у вас под рукой находится мод, основанный на Корсарах 3 - скорее всего, таких строк вы ни в одном диалоге не найдёте.
Я не буду заострять здесь внимание, потому что полноценно это работает лишь в оригинальном ПКМ. Хотите разобраться в тонкостях - изучайте файлы диалогов персонажей связанных с базовым сюжетом.
Если вы модифицируете ВМЛ или что-то на нём основанное - из всего этого добра будет лишь частично работать анимация портрета. Можете прописывать только её, либо не писать вообще ничего из этого блока.
Из всего данного блока поговорим мы только о последней строке - Diag.TempNode = "First time";
В первом уроке я говорил, что атрибуту TempNode присваивается значение "First time" еще при создании персонажа. Но если вы уже листали какие-то файлы диалогов, то наверняка заметили, что ваши предшественники-мододелы присваивают данное значение чуть ли не в каждом кейсе.
Зачем они это делают?
Если полистать диалоги с оригинального сюжета, можно заметить, что иногда атрибуту TempNode присваиваются другие значения (чаще всего "Second time"). Таким образом достигалась разная реакция на ГГ в зависимости от развития сюжета, с разными возможностями в диалоге. А чтобы не держать в голове эти исключения и всё работало как ожидается - разработчики явно задают значение TempNode в каждом кейсе.
Как правило, мододелы для всех этих ветвлений используют условные конструкции if/then/else, но большинство из них продолжает везде вставлять Diag.TempNode = "First time";.
Каким методом пользоваться и вставлять ли везде данное объявление - решать только вам.
Что происходит дальше:
switch(Dialog.CurrentNode) {
    case "First time":
        // ...

        if(NPChar.quest.meeting == "0") {           // проверяем, знаком ли ГГ с барменом
            dialog.snd = "Voice\CHWI\CHWI001";
            Dialog.Text = "Добро пожаловать, " + GetAddress_Form(NPChar) + ". Еда, выпивка и развлечения - все по умеренным ценам!";
            Link.l1 = "Посмотрим... Я " + GetFullName(pchar) + ".";
            Link.l1.go = "meeting";
            NPChar.quest.meeting = "1";             // помечаем, что мы познакомились
            Dialog.snd = "dialogs\speech\chrar001";
        } else {
            dialog.snd1 = "Voice\CHWI\CHWI002";     // выбор файлов озвучки
            dialog.snd2 = "Voice\CHWI\CHWI003";     // работает только в оригинальной ПКМ
            dialog.snd3 = "Voice\CHWI\CHWI004";
            Dialog.Text = RandPhrase("Эй, " + GetAddress_Form(NPChar) + " " + PChar.name + "! " + TimeGreeting() + ".",
                                "О, какие у нас гости! Рад видеть вас, " + GetAddress_Form(NPChar) + " " + PChar.name + ".",
                                "Хорошо, что вы заглянули ко мне, " + PChar.name + ". Каков будет сегодня ваш заказ?", &dialog, dialog.snd1, dialog.snd2, dialog.snd3);
            Link.l1 = "Есть на острове парни, готовые уйти в плавание?";
            Link.l1.go = "crew hire";
            Link.l2 = "Я надеюсь ты сможешь ответить на пару вопросов.";
            Link.l2.go = "quest lines";
            link.l3 = pcharrepphrase("Есть ли в твоем клоповнике свободная конура?", "Нет ли у тебя свободной комнаты, я хотел"+ GetSexPhrase("","а") +" бы остановиться здесь на некоторое время.");
            link.l3.go = "room";
            Link.l4 = "Увы, я уже ухожу, " + NPChar.name + ". До встречи.";
            Link.l4.go = "exit";
        }
    break;
}
А дальше нас встречает то, о чем я только-что говорил: вместо принципиально другой ноды диалога здесь используется условный оператор if, который проверяет значение переменной meeting.
Как можно догадаться из названия, отвечает она за приветствие. Если мы в этой таверне впервые - активируется первый блок, в котором бармен и ГГ представляются и обмениваются приветствиями.
Далее переменной meeting присваивается значение 1 и при всех последующих обращениях к данному бармену выводится стандартный диалог с тавернщиком из второго блока - else.
Детальнее рассмотрим этот блок.
Еще в предыдущем уроке вы могли заметить, что реплики как НПЦ (Dialog.Text), так и наши варианты ответов (link.l4) состоят не только из текста, но и каких-то выражений. Самое время о них поговорить.
В большинстве случаев это просто атрибуты, такие как имя персонажа PChar.name или название корабля PChar.Ship.Name. Но не редко встречаются и функции, как RandPhrase(string a, string b, string c) в данном случае.
Это вспомагательные ф-ции для работы с текстом, их довольно много и мы поговорим об этом в следующем уроке. Конкретно эта выбирает случайную строку из трёх переданных в функцию.
Далее идут наши варианты ответов: нанять команду, поговорить о задании, снять комнату и попрощаться.
В пределах этого урока мы поглядим на диалог найма команды. Он достаточно информативен и, в то же время, не сильно перегружен.
case "crew hire":
    // проверка на наличие корабля в порту
    if (sti(Pchar.Ship.Type) == SHIP_NOTUSED || Pchar.location.from_sea != NPChar.from_sea) {
        if (NPChar.from_sea != "") {
            Dialog.text = "А на что тебе матросы? Что-то не вижу твоего корабля в порту.";
            link.l1 = RandPhraseSimple("Точно... я его пришвартовал"+ GetSexPhrase("","а") +" не в том месте", "Забыл"+ GetSexPhrase("","а") +" войти в порт...");
            link.l1.go = "exit";
            break;
        }
    }
    // проверка на время суток
    if (makeint(environment.time) > 22.0 || makeint(environment.time) < 10.0) {
        dialog.snd = "Voice\CHWI\CHWI011";
        Dialog.text = "Обычно у меня в таверне полно людей, желающих стать матросами, но сейчас слишком поздно, и они начнут появляться только утром. Может быть, вы хотите снять комнату, и подождать их?";
        link.l1 = "Хорошо. У тебя есть свободные комнаты?";
        link.l1.go = "room";
        link.l2 = "Меня не интересует комната на данный момент. Давай сменим тему.";
        link.l2.go = "no quest";
    } else {
        // проверка на отношение нации к ГГ
        if (GetNationRelation2MainCharacter(sti(NPChar.nation)) == RELATION_ENEMY && sti(NPChar.nation) != PIRATE) {
            Dialog.text = "А ты думаешь к тебе кто-то пойдет, когда ты с нами во вражде? Скажи спасибо, что стражу не зову.";
            link.l1 = "Спасибо.";
            link.l1.go = "exit";
        } else {
            Diag.CurrentNode = Diag.TempNode;
            NPChar.quest.meeting = NPC_Meeting;
            DialogExit();
            // вызов окна найма команды
            LaunchHireCrew();
        }
    }
break;
Что мы здесь видим? Уже привычные нам элементы диалога обёрнуты во множество проверок.
Собственно, так большинство диалогов в игре и строится. Разработчики оригинальных Корсаров на каждый случай использовали отдельную ноду, а мододелы просто используют проверки. Нельзя сказать какой из вариантов правильный, а какой нет - они оба работают отлично. Делайте так, как вам удобно.
Так как все элементы приведенного кода вам уже знакомы, вкратце пробежимся по ключевым местам.
Мы пришли нанимать команду на корабль. А что если у нас нет корабля? Или он пришвартован где-то вообще на другом острове?
Первым делом, проверяем эти пункты.
Следом идет проверка на время суток. Кто же пойдёт устраиваться на работу в три часа ночи?
И в заключение - проверка на отношение нации с главным героем. Может мы вражеский шпион, пробравшийся в город под чужим флагом.
И только пройдя все эти проверки мы добираемся до ф-ции LaunchHireCrew(), которая вызывает окно найма команды.
По такому же принципу работают магазин, верфь и все остальные. Для всех их имеются аналогичные ф-ции. Мы поговорим о них в следующем уроке.

Если у вас остались какие-либо вопросы - вы можете задать их в коментариях или разделе Q/A.

Содержание
`
ОЖИДАНИЕ РЕКЛАМЫ...