Важно с самого начала понимать, что такой сущности как задание в игровом движке корсаров не существует. Вся система крутится вокруг обработки отдельно взятых кейсов в файле quests_reaction.c.
Пусть вас не смущает повсеместное использование в коде слова quest - речь всегда идет именно о конкретном сегменте задания (даже не этапе), воплощенного в отдельно взятом кейсе.
Пусть вас не смущает повсеместное использование в коде слова quest - речь всегда идет именно о конкретном сегменте задания (даже не этапе), воплощенного в отдельно взятом кейсе.
Это создаёт дополнительные трудности. Задание, как таковое, существует только в голове автора. В скриптах оно размазано по разным участкам файлов диалогов персонажей, которые в нём задействованы, а также различным кейсам в quests_reaction.c.
Одна из таких трудностей - чтение заданий третьими лицами (да и самим автором, спустя некоторое время). Куски кода раскиданы по разным файлам, чаще всего, никак между собой не связанным. Вы познакомитесь с этим ближе, когда мы будем разбирать реализацию реального задания из игры.
Полное отсутствие общей структуры кода заданий способствет тому, что авторы размещают фрагменты этого кода где угодно. Достаточно частое явление - части кода задания лежат не в quests_reaction.c, а прямо в файлах диалогов персонажей.
Полное отсутствие общей структуры кода заданий способствет тому, что авторы размещают фрагменты этого кода где угодно. Достаточно частое явление - части кода задания лежат не в quests_reaction.c, а прямо в файлах диалогов персонажей.
Фундаментом квестовой системы Корсаров являются условия. Практически все функции, задействованные в квестовой системе, крутятся вокруг обработки тех или иных условий.
Автор задания прописывает необходимые условия выполнения/провала для каждого кейса в quests_reaction.c, а обработчики затем проверяют выполнение этих условий. Так происходит до тех пор, пока все они не будут выполнены - тогда данный кейс отправляется на исполнение.
В ходе выполнения кейса, помимо самих квестовых действий, также прописывается следующий кейс в цепочке и его условия. Так происходит переход от одной части задания к другой.
В ходе выполнения кейса, помимо самих квестовых действий, также прописывается следующий кейс в цепочке и его условия. Так происходит переход от одной части задания к другой.
В ходе первых нескольких уроков мы разберём непосредственно техническое воплощение квестовой системы. Как игра воспринимает задание, как обрабатываются условия и что ещё, кроме озвученного, происходит.
Примечание:Если вы пишете только сюжет заданий, а их воплощением в коде занимаются другие, специально обученные, люди - вероятно, вам стоит пропустить уроки с разбором самой системы и перейти сразу к разбору реализации задания "Сопровождение Торговца".
Там вы получите информацию, необходимую для правильного понимания структуры задания, узнаете на какие сегменты его необходимо делить, как подбирать условия к этим сегментам, и прочую информацию, которая поможет сформировать базовое представление об особенностях реализации заданий в Корсарах. Это позволит быстрее написать квест таким образом, чтобы его не пришлось затем несколько раз переделывать, а можно было сразу передать кодеру на реализацию.
Структура системы обработки заданий
С чего всё начинается? С получения задания, конечно.
Чаще всего мы получаем квест в ходе диалога:
Чаще всего мы получаем квест в ходе диалога:
// questName это название кейса в quests_reaction.c
void AddDialogExitQuest(string questName)
{
string attrName;
aref ar;
if( CheckAttribute(pchar,"DialogExitQuests") )
{
makearef(ar,pchar.DialogExitQuests);
attrName = "l" + GetAttributesNum(ar);
}
else
{
attrName = "l0";
}
pchar.DialogExitQuests.(attrName) = questName;
}
Важный момент: своей структуры данных у системы заданий нет. Вся служебная, временная и прочая информация пишется в объект pchar (гланый персонаж).
Функция AddDialogExitQuest() вызывается из диалога. Сама по себе, она никаких заданий не запускает, а лишь добавляет переданное название квеста в список атрибутов "DialogExitQuests". Само название квеста, которое мы передаем, это имя кейса в файле quests_reaction.c, который будет выполнен после выхода из диалога.
Далее эта информация будет подхвачена обработчиком, который запускается по завершению диалога.
void QuestDialogExitProcedure()
{
int i = GetEventData(); // получаем данные из события
ref othepchar = GetCharacter(i); // получаем персонажа из индекса
aref ar, lref;
string attrName, Lname;
// эти функции вообще отключены, видимо какое-то наследие Акеллы
ExecuteAfterDialogTask(othepchar);
ExecuteAfterDialogTask(pchar);
// если есть задачи на исполнение по выходу из диалога
if( CheckAttribute(pchar,"DialogExitQuests") )
{
// получаем ссылку на список квестов
makearef(ar,pchar.DialogExitQuests);
// узнаем их количество
int iMax = GetAttributesNum(ar);
// перебираем циклом
for(i=0; i<iMax; i++)
{
// получаем текущий экземпляр
lref = GetAttributeN(ar,i);
// получаем имя квеста
attrName = GetAttributeValue(lref);
// получаем имя атрибута
Lname = GetAttributeName(lref);
// если указана функция
if (CheckAttribute(pchar, "DialogExitQuests." + Lname + ".function"))
{
// вызываем соответствующую функцию
call attrName();
}
else
{
// иначе - кейс в реакшене
CompleteQuestName(attrName, "");
}
// снимаем прерывание
if( CheckAttribute(pchar,"quest."+attrName+".win_condition") ) pchar.quest.(attrName).over = "yes";
}
// убираем обработку квестов после диалога
DeleteAttribute(pchar,"DialogExitQuests");
}
// вызываем проверку квестов
QuestsCheck();
}
Из урока по событиям мы знаем, что функция GetEventData() возвращает переданный в событие аргумент. Но что находится в этом аргументе?
Сам вызов функции QuestDialogExitProcedure() завязан на событие выхода из диалога:
Сам вызов функции QuestDialogExitProcedure() завязан на событие выхода из диалога:
SetEventHandler(EVENT_DIALOG_EXIT,"QuestDialogExitProcedure",0);
Данное событие, как не сложно догадаться, вызывается из функции DialogExit(), при помощи которой мы завершаем диалог. Мы не будем сейчас ее всю рассматривать, нас интересует только момент оповещения слушателей события:
PostEvent(EVENT_DIALOG_EXIT, 1, "l", sti(CharacterRef.index));
Это индекс персонажа, с которым мы вели диалог. Собственно, в следующей строке мы получаем ссылку на этого персонажа по его индексу. Но это всё неактуально, потому что функция, в которую этот персонаж передавался - отключена. Очевидно, какое-то наследие от кода Акеллы.
Нас интересует то, что происходит дальше. А дальше идет проверка наличия атрибута "DialogExitQuests", наличие которого означает наличие задач, которые необходимо выполнить по выходу из диалога.
Получаем необходимые атрибуты и в цикле вызываем для всего списка либо функцию (если таковая указана) в reaction_functions.c, либо соответствующий кейс в quests_reaction.c. Но кейс тоже вызывается не напрямую, а через прокладку CompleteQuestName():
void CompleteQuestName(string sQuestName, string qname)
{
if( CheckAttribute(&objQuestScene,"list."+sQuestName+".chrIdx") )
{
Event("qprocTaskEnd","a",GetCharacter(sti(objQuestScene.list.(sQuestName).chrIdx)));
}
else
{
QuestComplete(sQuestName, qname);
}
}
Эта функция запускает обработку квестовых сцен, если таковые указаны. Если нет - вызывает кейс в реакшене. В данном уроке сцены я разбирать не буду, поэтому останавливаться здесь не будем. Вернемся к QuestDialogExitProcedure(). Последней строкой в цикле ставится отметка "yes" в атрибут "over". Это нужно для того, чтобы снять прерывание для этого квеста. Подробнее об этом будет ниже.
После завершения цикла мы убираем атрибут "DialogExitQuests" и вызываем проверку квестов:
bool bQuestCheckProcess = false; // флаг процесса выполнения проверки квестов
bool bQuestCheckProcessFreeze = false; // флаг паузы проверки квестов
// проверка состояния квестов
void QuestsCheck()
{
// выход из ф-ции проверки, если выполняется одно из условий:
// - проверка уже в процессе выполнения
// - проверка заморожена
// - активен диалог
if(bQuestCheckProcess || bQuestCheckProcessFreeze || dialogRun) return; // условия запуска проверки
// устанавливаем флаг, что начат процесс проверки квестов
bQuestCheckProcess = true;
aref quests; // пул квестов
aref quest; // проверяемый в данный момент квест
aref conditions; // пул условий квеста
aref condition; // проверяемое в данный момент условие
int nQuestsNum; // счетчик квестов
int nConditionsNum; // счетчик условий
int n,m; // счетчики циклов
string sQuestName; // название квеста
bool bQuestCompleted; // флаг выполнения условий
// ссылка на список активных квестов
makearef(quests,pchar.quest);
// кол-во активных квестов
nQuestsNum = GetAttributesNum(quests);
// проход по всем активным квестам
for(n = 0; n < nQuestsNum; n++)
{
// дополнительная проверка на каждой итерации
// если начат диалог, или проверка заморожена - переход к следующей итерации
if (bQuestCheckProcessFreeze || dialogRun) continue;
// обращаемся к конкретному квесту по списку
quest = GetAttributeN(quests,n);
// получаем название текущего квеста
sQuestName = GetAttributeName(quest);
// проверка условий выполнения
if(CheckAttribute(quest,"win_condition"))
{
// если никаких условий не задано
if(quest.win_condition == "no")
{
// отправляем в обработчик выполнения квеста
OnQuestComplete(quest, sQuestName);
// пересчитываем кол-во активных квестов
nQuestsNum = GetAttributesNum(quests);
// вызываем следующую итерацию цикла
continue;
}
// получаем список условий квеста
makearef(conditions,quest.win_condition);
// ко-во условий
nConditionsNum = GetAttributesNum(conditions);
// если кол-во условий равно нулю
if(nConditionsNum == 0)
{
// повторяем процедуру отсутствия условий
OnQuestComplete(quest, sQuestName);
nQuestsNum = GetAttributesNum(quests);
continue;
}
// устанавливаем флаг что все условия выполнены
bQuestCompleted = true;
// проходим по всем условиям
for(m = 0; m < nConditionsNum; m++)
{
// обращаемся к текущему условию в списке
condition = GetAttributeN(conditions,m);
// вызывваем проверку текущего условия
if(ProcessCondition(condition) == false)
{
// если условие не выполняется - меняем флаг
bQuestCompleted = false;
// и прерываем цикл проверки условий
break;
}
}
// если все условия выполняются
if(bQuestCompleted)
{
// отправляем в обработчик выполнения квеста
OnQuestComplete(quest, sQuestName);
// пересчитываем кол-во активных квестов
nQuestsNum = GetAttributesNum(quests);
}
}
// проверяем условия провала квеста
if(CheckAttribute(quest,"fail_condition"))
{
// получаем список условий провала
makearef(conditions,quest.fail_condition);
// кол-во условий провала
nConditionsNum = GetAttributesNum(conditions);
// если кол-во условий провала равно нулю
// переходим к следующей итерации (следующему квесту)
if(nConditionsNum == 0) continue;
// проходим по всем условиям провала
for(m = 0; m < nConditionsNum; m++)
{
// обращаемся к текущему условию в списке
condition = GetAttributeN(conditions,m);
// если условие провала выполняется
if(ProcessCondition(condition) == true)
{
// отправляем в обработчик провала квеста
OnQuestFailed(quest, sQuestName);
// пересчитываем кол-во активных квестов
nQuestsNum = GetAttributesNum(quests);
// прерываем цикл проверки условий провала
break;
}
}
}
}
// кол-во активных квестов
nQuestsNum = GetAttributesNum(quests);
// проходим по всем активным квестам
for(n = 0; n < nQuestsNum; n++)
{
// обращаемся к текущему квесту в списке
quest = GetAttributeN(quests,n);
// проверяем атрибут завершения квеста
if(CheckAttribute(quest,"over") && quest.over=="yes")
{
// удаляем выполненный или проваленный квест
DeleteAttribute(quests,GetAttributeName(quest));
// отнимаем его от счетчика цикла
n--;
// и от кол-ва квестов в списке
nQuestsNum--;
}
}
// снимаем флаг процесса проверки квестов
bQuestCheckProcess = false;
}
// обработчик выполнения задания
void OnQuestComplete(aref quest, string sQuestname)
{
// если нет атрибута "квест завершен"
// и есть атрибут "условия выполнения"
if(!CheckAttribute(quest,"over") && CheckAttribute(quest,"win_condition"))
{
// если квест НЕ повторяющийся (не генераторный)
if(!CheckAttribute(quest,"again"))
{
// устанавливаем атрибут что квест закончен
// по которому снимается прерывание
quest.over = "yes";
}
// вызываем кейс выполнения квеста в quests_reaction.c
QuestComplete(quest.win_condition, sQuestName);
}
}
// обработчик провала задания
void OnQuestFailed(aref quest, string sQuestName)
{
// если у квеста есть атрибут условия провала
if(CheckAttribute(quest,"fail_condition"))
{
// устанавливаем атрибут что квест закончен
quest.over = "yes";
// вызываем кейс провала квеста в quests_reaction.c
QuestComplete(quest.fail_condition, sQuestName);
}
}
И это первая глобальная проблема квестовой системы Корсаров - повсеместная прогонка ВСЕХ квестов по каждому чиху. Более предметно мы об этом поговорим в уроке посвященному доработке и улучшениям данной системы.
Я максимально снабдил комментариями весь код функции, поэтому описываю процесс в кратце.
QuestsCheck(), как вы уже поняли, перебирает циклом все активные квесты. Сначала проверяются условия победы ("win_condition"). Если таковых не задано - происходит вызов соответствующего кейса в quests_reaction.c через дополнительную функцию OnQuestComplete(). Её код также описан выше. Если все условия победы выполняются - то же самое. Если условия победы заданы, но не выполнены - переходим к проверке условий провала ("fail_condition"). Если хоть одно условие провала выполняется - вызываем кейс провала задания (тоже через дополнительную ф-цию OnQuestFailed()).
Теперь о самой проверке условий. Она выполняется функцией ProcessCondition():
// проверка условия
bool ProcessCondition(aref condition)
{
bool bTmp;
int i;
int iNation, iLocation; // индексы нации, локации
ref tmpRef;
ref refCharacter; // персонаж
string sConditionName; // тип условия
string sTmpString; // группа НПЦ
string locGroup; // группа локаторов
string sLocation; // локация
float fx,fy,fz; // координаты
// получаем тип условия
sConditionName = GetAttributeValue(condition);
// если есть условие по персонажу
if(CheckAttribute(condition,"character"))
{
// Алексусом было наворочено несколько дополнительных проверок
// для совместимости с кодом Акеллы
// где в атрибуте "character" хранили индекс (в массиве персонажей)
// а не идентификатор (строка), по которому сейчас определяются персонажи
// пытаемся получить индекс персонажа по идентификатору (имени)
i = GetCharacterIndex(condition.character);
// если удалось - значит там идентификатор (строка)
if (i != -1) condition.characterIdx = i;
// если нет - значит там изначально был записан индекс (число)
else condition.characterIdx = sti(condition.character);
// удаляем этот атрибут и далее работаем с индексом
DeleteAttribute(condition,"character");
}
// если есть условие по персонажу
if(CheckAttribute(condition,"characterIdx"))
{
// получаем ссылку на этого персонажа
refCharacter = GetCharacter(sti(condition.characterIdx));
}
else
{
// иначе - работаем с главным героем
refCharacter = GetMainCharacter();
}
// проверяем условие
switch(sConditionName)
{
// выход на глобальную карту
case "MapEnter":
// проверяем, инициализирована ли сущность "worldMap"
return IsEntity(worldMap);
break;
// выход из локации
case "ExitFromLocation":
// возвращает TRUE если локация, где находится персонаж, отличается от заданной
return refCharacter.location != condition.location;
break;
// вход на локацию
case "location":
// если локация, на которой находится персонаж, соответствует заданной
if(refCharacter.location==condition.location)
{
// отключаем генерацию наземных энкаунтеров (чтоб не мешали квесту)
bLandEncountersGen = false;
// возвращаем TRUE если главный герой жив
return !CharacterIsDead(refCharacter);
}
// иначе - возвращаем FALSE
return false;
break;
// ограничение по времени (таймер)
case "Timer":
// если вы внимательно посмотрите, как засчитывается выполнение данного условия
// это может вызвать когнитивный диссонанс
// дело в том, что таймеры задаются не в fail_condition, а в win_condition
// и для обработки такого провала регистрируется как бы отдельный квест со своим
// отдельным кейсом в quests_reaction
// поэтому пересечение границы времени засчитывается как TRUE
// проверяем год
if( GetDataYear() < sti(condition.date.year) ) return false;
if( GetDataYear() > sti(condition.date.year) ) return true;
// проверяем месяц
if( GetDataMonth() < sti(condition.date.month) ) return false;
if( GetDataMonth() > sti(condition.date.month) ) return true;
// проверяем день
if( GetDataDay() < sti(condition.date.day) ) return false;
if (CheckAttribute(condition, "date.hour") && GetDataDay() <= sti(condition.date.day)) //fix
{
// и даже конкретное время суток
if(GetHour() < stf(condition.date.hour)) return false;
if(GetHour() >= stf(condition.date.hour)) return true;
}
// а это на случай, если что-то пошло мимо наших проверок
return true;
break;
// нахождение в определенном локаторе
case "locator":
// проверяем, что персонаж находится в нужной локации
if(refCharacter.location == condition.location)
{
// получаем группу локаторов
locGroup = condition.locator_group;
// добавляем в атрибут проверки данную группу локаторов
// если она туда еще не добавлена
if( !CheckAttribute(refCharacter,"Quests.LocatorCheck."+locGroup) )
{
// для начала, проверяем существует ли указанный персонаж
if(IsEntity(refCharacter))
{
// создаем атрибут
refCharacter.Quests.LocatorCheck.(locGroup) = "";
// получаем координаты персонажа
if( GetCharacterPos(refCharacter,&fx,&fy,&fz) )
{
// проверяем, находятся ли эти координаты
// в указанной группе локаторов
if( CheckCurLocator(locGroup,condition.locator, fx,fy,fz) )
// если ДА - заносим в этот атрибут целевой локатор
// для прохождения проверки условия
refCharacter.Quests.LocatorCheck.(locGroup) = condition.locator;
}
// добавляем детектор по группе локаторов
AddCharacterLocatorGroup(refCharacter,locGroup);
}
else
{
// если персонажа не существует - выводим ошибку в лог
Trace("character "+refCharacter.id+" not entity");
// и возвращаем FALSE
return false;
}
}
// срвниваем локатор персонажа с целевым
if(refCharacter.Quests.LocatorCheck.(locGroup)==condition.locator) return true;
}
// если локация не совпадает - возвращаем FALSE
return false;
break;
// смерть персонажа
case "NPC_Death":
// проверяем, мертв ли указанный персонаж
return CharacterIsDead(refCharacter);
break;
//тип локации
case "Location_Type":
// проверяем инициализирована ли локация
if (IsEntity(loadedLocation))
{
// сравниваем тип локации с целевым
// и жив ли персонаж
if (loadedLocation.type == condition.location_type) return !CharacterIsDead(refCharacter);
}
// иначе возвращаем FALSE
return false;
break;
// колония принадлежит определенной нации
case "Nation_City":
// проверяем инициализирована ли локация
if (IsEntity(loadedLocation))
{
// убеждаемся, что данная локация - это город
if (loadedLocation.type == "town")
{
// получаем индекс целевой нации
iNation = sti(condition.nation);
// получаем локацию
sLocation = refCharacter.location;
// получаем индекс локации
iLocation = FindLocation(sLocation);
// проверяем наличие атрибута принадлежности к городу
if(CheckAttribute(&Locations[iLocation], "fastreload"))
{
// получаем название города
sLocation = Locations[iLocation].fastreload;
// проверяем нацию-владельца города
if(iNation == sti(Colonies[FindColony(sLocation)].nation)) return true;
}
}
}
// иначе возвращаем FALSE
return false;
break;
// наличие товара в трюме
case "Goods":
// проверяем, что в трюме есть указанный тип товара
// в указанном количестве
return TestIntValue(GetCargoGoods(refCharacter,sti(condition.goods)),sti(condition.quantity),condition.operation);
break;
// наличие предмета у персонажа
case "item":
// проверяем наличие у персонажа целевого предмета
return CheckCharacterItem(refCharacter,condition.item);
break;
// убийство в ходе абордажа
case "Character_Capture":
// проверяем наличие у персонажа атрибута с типом убийства
// и тип убийства - убит в ходе абордажа
if( CheckAttribute(refCharacter,"Killer.status") && sti(refCharacter.Killer.status)==KILL_BY_ABORDAGE ) return true;
// иначе возвращаем FALSE
return false;
break;
// убийство потоплением
case "Character_sink":
// проверяем наличие у персонажа атрибута с типом убийства
// и тип убийства - не в ходе абордажа
if( CheckAttribute(refCharacter,"Killer.status") && sti(refCharacter.Killer.status) != KILL_BY_ABORDAGE ) return true;
// иначе возвращаем FALSE
return false;
break;
// локация стоянки корабля
case "Ship_location":
// проверяем наличие атрибута припаркованного корабля
// и что корабль находится в указанной локации
if( CheckAttribute(refCharacter,"location.from_sea") && refCharacter.location.from_sea==condition.location ) return true;
// иначе возвращаем FALSE
return false;
break;
// уничтожение группы персонажей
case "Group_Death":
// получаем целевую группу
sTmpString = condition.group;
// проверяем, что группа мертва
return Group_isDead(sTmpString);
break;
// прибытие к острову
// вход в акваторию??
case "ComeToIsland":
// а это тот случай, когда роль играет просто наличие атрибута
// никаких полезных данных в него не заносится
// проверяем наличие атрибута "ComeToIsland"
// который проставляется при входе в акваторию какого-нибудь острова
if(CheckAttribute(refCharacter,"ComeToIsland") && refCharacter.ComeToIsland=="1")
{
// удаляем атрибут
DeleteAttribute(refCharacter, "ComeToIsland");
// и возвращаем TRUE
return true;
}
// иначе возвращаем FALSE
return false;
break;
// выход на боевую карту (море)
case "EnterToSea":
// проверяем глобальную переменную, которая отвечает за морсую пену (??)
if(bSeaActive == true)
{
// возвращаем TRUE
return true;
}
// иначе возвращаем FALSE
return false;
break;
// выход с боевой карты
case "ExitFromSea":
// та же переменная, только в этот раз проверяем что она неактивна
if (bSeaActive == false)
{
// возвращаем TRUE
return true;
}
// иначе возвращаем FALSE
return false;
break;
// принадлежность локации определенной нации
case "nation_location":
// индекс нации
iNation = sti(condition.nation);
// получаем локацию, в которой находится персонаж
sLocation = refCharacter.location;
// индекс локации
iLocation = FindLocation(sLocation);
// если локация с таким индексом существует
if(iLocation != -1)
{
// проверяем наличие атрибута принадлежности к городу
if(CheckAttribute(&Locations[iLocation], "fastreload"))
{
// получаем название города
sLocation = Locations[iLocation].fastreload;
// индекс нации-владельца города
int iCurrentNation = sti(Colonies[FindColony(sLocation)].nation);
// сравниваем индексы целевой нации и владельца города
if(iNation == iCurrentNation)
{
// возвращаем TRUE
return true;
}
}
}
// иначе возвращаем FALSE
return false;
break;
// захват форта
case "Fort_capture":
// проверяем наличие атрибута "FortCapture"
// который выдается при захвате форта
if( CheckAttribute(refCharacter,"FortCapture") && refCharacter.FortCapture=="1" )
{
// удаляем атрибут
DeleteAttribute(refCharacter, "FortCapture");
// возвращаем TRUE
return true;
}
// иначе возвращаем FALSE
return false;
break;
// захват корабля
case "Ship_capture":
// проверяем наличие атрибута "ShipCapture"
// который выдается при захвате корабля
if(CheckAttribute(refCharacter,"ShipCapture") && refCharacter.ShipCapture=="1")
{
// удаляем атрибут
DeleteAttribute(refCharacter, "ShipCapture");
// возвращаем TRUE
return true;
}
// иначе возвращаем FALSE
return false;
break;
// нахождение в координатах
case "Coordinates":
// здесь реализованы отдельные проверки глобальной и боевой карты
// проверяем инициализирована ли сущность "worldMap"
if(IsEntity(worldMap))
{
// сравниваем заданные координаты с фактическим
// положением корабля на глобальной карте
if( GetMapCoordDegreeX(makefloat(worldMap.playerShipX)) == sti(condition.coordinate.degreeX) &&
GetMapCoordDegreeZ(makefloat(worldMap.playerShipZ)) == sti(condition.coordinate.degreeZ) &&
GetMapCoordMinutesX(makefloat(worldMap.playerShipX)) == sti(condition.coordinate.minutesX) &&
GetMapCoordMinutesZ(makefloat(worldMap.playerShipZ)) == sti(condition.coordinate.minutesZ))
{
// если совпадают - возвращаем TRUE
return true;
}
// иначе - возвращаем FALSE
else return false;
}
else
{
// проверяем переменную моря
// а также переменную абордажа
if (bSeaActive && !bAbordageStarted)
{
// проверяем наличие атрибута координат корабля
if (CheckAttribute(pchar, "Ship.pos.x"))
{
// сравниваем заданные координаты с фактическим
// положением корабля на боевой карте
if( GetSeaCoordDegreeX(makefloat(pchar.Ship.pos.x)) == sti(condition.coordinate.degreeX) &&
GetSeaCoordDegreeZ(makefloat(pchar.Ship.pos.z)) == sti(condition.coordinate.degreeZ) &&
GetSeaCoordMinutesX(makefloat(pchar.Ship.pos.x)) == sti(condition.coordinate.minutesX) &&
GetSeaCoordMinutesZ(makefloat(pchar.Ship.pos.z)) == sti(condition.coordinate.minutesZ))
{
// если совпадают - возвращаем TRUE
return true;
}
// иначе - возвращаем FALSE
else return false;
}
}
}
break;
}
// если ни один кейс не совпал - пишем в лог ошибку неизвестного типа условия
trace("ERROR: unidentified condition type()" + condition);
// и возвращаем FALSE
return false;
}
При создании квеста мы задаём некоторые условия (из доступных) его выполнения или провала. Данная функция как раз занимается проверкой этих условий. Если условие выполнено - возвращает TRUE, если нет - FALSE.
Я постарался прокомментировать каждую строчку данной функции, чтобы вы могли изучить какие условия вообще доступны и как устроена их проверка.
И последний этап - это, собственно, выполнение кейса в quests_reaction.c. Я не буду копировать сюда все 12 тысяч строк - это не имеет смысла. Нас интересует только сама функция:
void QuestComplete(string sQuestName, string qname)
{
// различные переменные, использующиеся в квестах
ref sld, npchar;
aref arOldMapPos, arAll, arPass;
int iTemp, i, ShipType, Rank;
float locx, locy, locz, fTemp;
string attrName, Model, Blade, Gun, sTemp, sQuestTown, sQuestTitle;
bool bOk;
int iChurchGenBanditsCount;
// вывод логов
if (bQuestLogShow)
{
Log_Info("Quest completed : " + sQuestName + " param = " + qname);
trace("Quest completed : " + sQuestName + " param = " + qname + " " + GetQuestBookDataDigit());
}
// запуск функции вместо кейса, если таковая указана
if (CheckAttribute(pchar, "quest." + qname + ".function"))
{
string sFunction = pchar.quest.(qname).function;
call sFunction(qname);
return;
}
switch(sQuestName)
{
// здесь пошли различные кейсы заданий
case "Rand_Smuggling":
pchar.quest.KillSmugglers_after.over = "yes";
RemoveSmugglersFromShore();
break;
// ...
}
}
Ничего хитроумного тут нет. Есть настроенный вывод логов, что поможет вам в тестировании своих квестов.
Есть оставшийся от предыдущих аддонов вызов функции вместо кейса, хотя здесь это реализовано еще на этапе диалога, когда вместо AddDialogExitQuest() вызывается AddDialogExitQuestFunction(). Тот же костыль, только в профиль. А далее, собственно, идут все наши кейсы.
Есть оставшийся от предыдущих аддонов вызов функции вместо кейса, хотя здесь это реализовано еще на этапе диалога, когда вместо AddDialogExitQuest() вызывается AddDialogExitQuestFunction(). Тот же костыль, только в профиль. А далее, собственно, идут все наши кейсы.
В реализации, которую я сейчас ковыряю, функции вынесены в отдельный файл reaction_functions.c, что сокращает кол-во строк в quests_reaction.c с 12 до 8 тысяч. Но глобально проблему это не решает - держать десятки тысяч строк в функции, которая вызывается повсеместно - это вторая большая боль данной системы.