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

Разбор квеста "Сопровождение Судна"

Содержание:

Сопровождение судна

Начинается всё действо в диалоге с тавернщиком:
// ..\PROGRAM\DIALOGS\Common_Tavern.c
case "convoy":
    // ...
    
    dialog.text = "Ты вовремя ко мне "+ GetSexPhrase("обратился парень","обратилась") +", тут один купец как раз искал подходящую компанию, а вот, как раз и он, видишь, вошел в таверну? Поговори с ним.";
    link.l1 = "Я так и сделаю, не сомневайся.";
    link.l1.go = "exit";
    pchar.quest.destination = GetIslandByCityName(NPChar.city); // остров, где взяли квест
    AddDialogExitQuest("prepare_for_convoy_quest");             // активация квеста при выходе из диалога
    
    // ...
break;
По сюжету квеста мы спрашиваем работу у трактирщика и получаем наводку на торговца, который просит сопроводить нас до какого-то острова.
Что мы здесь видим?
Во-первых, нас перебрасывают на ноду диалога "exit". И ничего странного в этом нет, потому что задание мы берём (или не берём) непосредственно у торговца, а не у трактирщика.
А далее, собственно, происходит квестовая магия. В атрибут pchar.quest.destination сохраняется остров, на котором мы получили задание, чтобы исключить его из списка потенциальных точек назначения в будущем. И это первый пример плохого кода. Данное действие - исключительно квестовое, которое не имеет абсолютно никакого отношения к диалогу, и находиться оно должно в файле quests_reaction.c.
Более того, разбрасывание частей кода не там, где ему положено находиться, приводит к затруднениям в работе с этим самым кодом. Когда вы (или кто-то другой), спустя некоторе время будете пытаться изменить что-то в этом квесте - все его экшены вы будете искать в квест-реакшн. И не найдёте там нужного куска кода, потому что он блт в диалоге! Или может вы не будете ничего искать, а просто что-то добавите/измените, а оно будет работать не так, как ожидается, потому что где-то существует код, который вы не видели и не учли.
Метод AddDialogExitQuest("prepare_for_convoy_quest"); запускает обработку квеста с переданным названием после завершения диалога.
В этом уроке мы не будем разбирать техническое устройство квестовой системы, а сразу перейдём к точке назначения - это файл quests_reaction.c. А именно - кейс с названием, которое мы передали в метод AddDialogExitQuest.
// ..\PROGRAM\QUESTS\quests_reaction.c
case "prepare_for_convoy_quest":
    sld = characterFromID("Quest trader");  // получаем алиас персонажа по ID
    SetFantomParam(sld);                    // генерируем ему уровень и навыки
    iPassenger = rand(4);                   // генерируем нацию
    
    sld.nation    = iPassenger;
    sld.BakNation = sld.nation;
    sld.location = "none";
    
    SetCaptanModelByEncType(sld, "trade");  // генерируем модель персонажа
    SetRandomNameToCharacter(sld);          // генерируем имя
    
    ChangeCharacterAddressGroup(sld, pchar.location, "reload", "reload1");  // телепортируем торговца в локацию ГГ
    
    pchar.quest.generate_convoy_quest_progress = "begin";               // прогресс задания
    LAi_SetActorType(sld);                                              // передаём управление персонажем под контроль компьютера
    LAi_SetActorType(pchar);
    LAi_ActorFollow(pchar, sld, "pchar_back_to_player", 6.0);           // приказ идти к другому персонажу
    LAi_ActorFollow(sld, pchar, "prepare_for_convoy_quest_2", 5.0);
break;
У вас, наверное, возникает вопрос, как мы перескочили от диалога с тавернщиком до получения ссылки на персонажа? Где же он был создан? В тех глубинах устройства квестовой системы, которые я не стал расписывать?
Нет. И это уже пример хорошего кода. Данный квест - повторяющийся. И чтобы не создавать и не описывать каждый раз нашего торговца, он создаётся один раз в начале игры, а при взятии квеста ему меняется внешность и он телепортируется к нам в таверну.
Расположен он в файле ..\PROGRAM\Characters\init\TempQuestCharacters.c, найти его можно по идентификатору "Quest trader".
Далее при помощи ф-ции ChangeCharacterAddressGroup() его помещают в локацию к нашему персонажу.
В атрибут pchar.quest.generate_convoy_quest_progress записывается прогресс задания. На данном этапе это "begin".
Следом идут методы AI-управления персонажами: LAi_SetActorType() передает управление персонажем под контроль AI, а метод LAi_ActorFollow() заставляет идти к другому персонажу и запускает задание (если передано).
Главному герою мы передали "pchar_back_to_player" - это заглушка, в которую зашита ф-ция, что возвращает игроку контроль над ГГ. Вот её код:
case "pchar_back_to_player":
    Lai_SetPlayerType(pchar);
break;
А торговцу было передано "prepare_for_convoy_quest_2" - это название следующего кейса по нашему заданию. Смотрим из чего он состоит:
// ..\PROGRAM\QUESTS\quests_reaction.c
case "prepare_for_convoy_quest_2":
    LAi_ActorDialog(characterFromID("Quest trader"), pchar, "", 10.0, 1.0);                     // приказ начать диалог
    characters[GetCharacterIndex("quest trader")].dialog.currentnode = "prepare_convoy_quest";  // указание ноды диалога
break;
Ничего неожиданного. Персонажи сблизились и далее мы инициируем между ними диалог.
Если вы уже смотрели инициацию нашего торговца в TempQuestCharacters.c, то знаете в каком файле этот самый диалог искать. Если нет - самое время это сделать. Напомню, что купец скрывается под id "Quest trader", а файл его диалога записан в атрибут ch.Dialog.Filename.
// ..\PROGRAM\DIALOGS\convoy_trader.c
case "prepare_convoy_quest":
    dialog.text = TimeGreeting() + ", "+GetAddress_Form(NPChar) + "! Я "+ GetFullName(NPChar) + ", торговец. Я слышал, что вы ищете работу?";
    link.l1 = "Что-то вроде того. А вы, как я слышал"+ GetSexPhrase("","а") +", ищете капитана, который бы сопроводил вас и ваше судно к месту назначения?";
    link.l1.go = "prepare_convoy_quest_2";
break;

case "prepare_convoy_quest_2":
    dialog.text = "Совершенно верно. Более того, думаю, что вы мне подходите в качестве сопровождающе"+ GetSexPhrase("го","й") +". Что скажете?";
    link.l1 = "Я скажу - назови мне сумму, и, возможно, мы договоримся.";
    link.l1.go = "prepare_convoy_quest_3";
break;

case "prepare_convoy_quest_3":
    GenerateConvoyQuestSwp();
    dialog.text = "Мне нужно, что бы меня сопроводили до города " + GetCityName(pchar.quest.destination) +
                  " за "+ pchar.ConvoyQuest.iDay +" дней, и за это я заплачу вам " + pchar.ConvoyQuest.convoymoney + " золотом. Что скажете?";
    link.l1 = "Я "+ GetSexPhrase("согласен","согласна") +".";
    link.l1.go = "convoy_agreeded";
    link.l2 = "Не думаю, что мне это интересно.";
    link.l2.go = "convoy_refused";
break;

case "convoy_refused":
    Diag.CurrentNode = Diag.TempNode;
    DialogExit();
    AddDialogExitQuest("convoy_refused");   // отмена задания
break;

case "convoy_agreeded":
    pchar.convoy_quest = pchar.quest.destination;
    Diag.CurrentNode = Diag.TempNode;
    DialogExit();
    AddDialogExitQuest("convoy_agreeded");  // принятие задания
break;

case "complete_convoy_quest":
    dialog.text = "О! Спасибо вам. Под вашей защитой я чувствовал себя как никогда спокойно. Вот ваша награда.";
    Link.l1 = "Благодарю вас.";
    link.l1.go = "exit";
    AddDialogExitQuest("convoy_refused");
    OfficersReaction("good");
    AddCharacterExpToSkill(pchar, "Sailing", 40);
    AddCharacterExpToSkill(pchar, "Leadership", 20);
    AddQuestTemplate("Gen_convoy_quest", "t4");
    CloseQuestHeader("Gen_convoy_quest");
break;
По порядку.
Из квест-реакшн нас направляют на самую первую ноду диалога - "prepare_convoy_quest". Далее по разговору мы переходим на вторую, третью и уже в ней получаем выбор - принять или отклонить задание.
Но ещё перед тем, как мы сделаем выбор, запускается некая ф-ция GenerateConvoyQuest(). Чтобы ответить на вопрос, почему она выполняется еще ДО самого тела ноды диалога, давайте сначала посмотрим на саму функцию:
// ..\PROGRAM\QUESTS\quests_functions.c
void GenerateConvoyQuest()
{
	ref PChar;
	ref NPChar;
	PChar = GetMainCharacter();
	int iShipType, iCargoType, iTradeGoods, iTradeMoney, iNation, irank;
	string sdestination;
	irank = PChar.rank;

	NPChar = characterFromID("Quest trader");

	DeleteAttribute(NPChar, "Ship");    // удаляем корабль от предыдущего квеста
    SetShipMerchant(NPChar, true);      // генерируем новый корабль

	iTradeMoney = (7-GetCharacterShipClass(npchar))*1000 + sti(NPChar.rank)*200 + rand(10)*50;
	
	pchar.ConvoyQuest.convoymoney = iTradeMoney;    // сумма награды
	pchar.ConvoyQuest.iDay  = 20 + rand(10);        // сроки выполнения

	SetTimerCondition("generate_convoy_quest_timer", 0, 0, sti(pchar.ConvoyQuest.iDay), false); // устанавливаем таймер на задание

	pchar.quest.generate_convoy_quest_progress = "begin";   // прогресс задания

	pchar.quest.generate_convoy_quest_failed.win_condition.l1 = "NPC_Death";                    // условия провала задания - смерть персонажа
	pchar.quest.generate_convoy_quest_failed.win_condition.l1.character = "Quest trader";       // указываем какого именно персонажа отслеживать
	pchar.quest.generate_convoy_quest_failed.win_condition = "generate_convoy_quest_failed";    // кейс задания, который запустится в случае выполнения этого условия

 	sdestination = GenerateDestination(NPChar, sti(NPChar.nation));

	if (sdestination == "")
	{
		sDestination = "Nevis";
	}

	pchar.quest.destination = sdestination; // пункт назначения
}
Как видим, эта функция создаёт все условия задания: вычисляет сумму награды, сроки выполнения, пункт назначения. А также описывает условия провала.
Теперь вернёмся к диалогу и посмотрим на него внимательно:
// ..\PROGRAM\DIALOGS\convoy_trader.c
case "prepare_convoy_quest_3":
    GenerateConvoyQuest();
    dialog.text = "Мне нужно, что бы меня сопроводили до города " + GetCityName(pchar.quest.destination) +
                  " за "+ pchar.ConvoyQuest.iDay +" дней, и за это я заплачу вам " + pchar.ConvoyQuest.convoymoney + " золотом. Что скажете?";
    link.l1 = "Я "+ GetSexPhrase("согласен","согласна") +".";
    link.l1.go = "convoy_agreeded";
    link.l2 = "Не думаю, что мне это интересно.";
    link.l2.go = "convoy_refused";
break;
Когда торговец предлагает нам работу, он уже указывает куда его нужно сопроводить, как быстро он должен оказаться в пункте назначения и сколько он за это платит.
Именно поэтому задание сгенерировалось ещё до нашего решения за него взяться.
С этим разобрались, едем дальше. Соглашаемся взяться за работу:
// ..\PROGRAM\DIALOGS\convoy_trader.c
case "convoy_agreeded":
    pchar.convoy_quest = pchar.quest.destination;
    Diag.CurrentNode = Diag.TempNode;
    DialogExit();
    AddDialogExitQuest("convoy_agreeded");  // кейс принятия задания
break;
Здесь нас встречает уже знакомый метод активации задания по выходу из диалога, который отправляет нас к кейсу "convoy_agreeded" в квест-реакшене:
// ..\PROGRAM\QUESTS\quests_reaction.c
case "convoy_agreeded":
    SetCompanionIndex(Pchar, -1, GetCharacterIndex("quest trader"));            // назначаем торговца компаньоном
    SetCharacterRemovable(characterFromID("quest trader"), false);              // меняем ему флаг removable
    characters[GetCharacterIndex("quest trader")].CompanionEnemyEnable = true;  // активируем возможность стать враждебным при попытке его захвата
    GetCharacterPos(GetMainCharacter(), &locx, &locy, &locz);
    homelocator = LAi_FindNearestFreeLocator("reload", locx, locy, locz);       // находим ближайший свободный локатор в локации ГГ
    LAi_SetActorType(characterFromID("quest trader"));
    LAi_ActorGoToLocation(characterFromID("quest trader"), "reload", homelocator, "none", "", "", "", 10.0);    // отправляем торговца на выход из локации
                                                                                // журнал заданий:
    ReOpenQuestHeader("Gen_convoy_quest");                                      // копируем раздел с квестом (т.к. он повторяемый)
    sTemp = AddQuestTemplate("Gen_convoy_quest", "t1");                         // подгружаем шаблон описания
    Pchar.QuestInfo.Gen_convoy_quest.t1.(sTemp).City  = GetCityName(pchar.quest.destination);   // указываем пункт назначения
    Pchar.QuestInfo.Gen_convoy_quest.t1.(sTemp).Day   = pchar.ConvoyQuest.iDay; // указываем сроки
    
    sDest = GetPortByCityName(pchar.quest.destination);
    
    pchar.quest.generate_convoy_quest_completed.win_condition.l1 = "Location";      // условие выполнения - вход в локацию
    pchar.quest.generate_convoy_quest_completed.win_condition.l1.location = sDest;  // в качестве локации указываем порт нужной колонии
    pchar.quest.generate_convoy_quest_completed.win_condition = "generate_convoy_quest_completed";  // кейс задания, который запустится в случае выполнения этого условия
break;
В этом кейсе происходят все остальные действия по запуску задания, которые небыли совершены в предварительной генерации:
Присоединяем торговца к эскадре игрока. Обратите внимание, что идентификатор ему присваивается не первый свободный, а -1.
Меняем ему значение removable на false, чтобы игрок не мог его уволить, как обычного офицера.
Разблокируем возможность стать враждебным. Это нужно для того, чтобы он вступал с нами в бой, если мы попытаемся атаковать или захватить его корабль.
Далее идут визуализационные активности: находим ближайший свободный локатор (а это будет дверь таверны, в которую он вошёл) и отправляем его выйти в эту дверь и перемещаем на несуществующую локацию, чтобы он исчез.
Следом идут записи в журнал заданий и добавление условий выполнения нашему квесту.
Журнал заданий (квест бук) лежит по пути ..\RESOURCE\INI\TEXTS\RUSSIAN\QuestBook.txt. Инетересующие нас строки находятся по запросу Gen_convoy_quest. Выглядит это всё следующим образом:
// ..\RESOURCE\INI\TEXTS\RUSSIAN\QuestBook.txt
Gen_convoy_quest_title {Сопровождение купца}
Gen_convoy_quest_t1
{
Я взялся сопроводить торговца до города #sCity# за #sDay# дней.
}
Gen_convoy_quest_t2
{
Торговец устал плавать со мной и ждать, когда я соизволю его сопроводить. Он вышел из моей эскадры.
}
Gen_convoy_quest_t3
{
Торговец мертв. Задание полностью провалено.
}
Gen_convoy_quest_t4
{
Задание выполнено.
}
Детали работы с квест-буком я буду разбирать в одном из следующих уроков. Здесь нам нужно лишь понимание, откуда берутся все эти записи. В данный момент мы достаем из этого файла запись с пометкой t1.
Далее предполагается, что мы выходим в море и сопровождаем торговца до пункта назначения.
Судя по коду, который мы видели, по прибытии в указанную локацию должен запуститься кейс "generate_convoy_quest_completed":
// ..\PROGRAM\QUESTS\quests_reaction.c
case "generate_convoy_quest_completed":
    homelocation = pchar.location;
    PlaceCharacter(characterFromID("quest trader"), "goto", homelocation);  // помещаем торговца в локацию ГГ
    LAi_SetActorType(characterFromID("quest trader"));                      // передаём управление торговцем
    LAi_SetActorType(pchar);                                                // и ГГ под контроль компьютера
    
    Pchar.GenQuest.Hunter2Pause = true;                                     // ставим на паузу охотников за головами (ОЗГ)
    
    DoQuestCheckDelay("pchar_back_to_player", 25.0);                        // возврат контроля игроку через 25сек
    LAi_ActorFollow(pchar, characterFromID("quest trader"), "", 2.0);       // направляем ГГ к торговцу
    LAi_ActorFollow(characterFromID("quest trader"), pchar, "generate_convoy_quest_completed_2", 2.0);  // а его к ГГ
break;
Когда мы оказываемся на причале в нужном порту, к нам должен подойти сопровождаемый торговец, поблагодарить и отдать награду.
Это то мы и наблюдаем в приведённом кейсе - торговец помещается к нам в локацию и отправляется идти к ГГ с последующим запуском кейса "generate_convoy_quest_completed_2".
Но перед этим есть пара интересных строк. Во-первых это ОЗГ. Их нужно поставить на паузу, чтобы (если они есть) они не порубили нас (и торговца) пока мы ведём диалог.
Вторая - возврат контроля спустя 25 секунд. Особенности движка таковы, что вполне реальной может быть ситуация, когда между вами и торговцем окажется несколько стражников (или ваших офицеров) и вы никогда не сможете до него дойти по узкому причалу.
Обе эти строки - это меры безопасности, которые появились здесь намного позже создания самого квеста.
// ..\PROGRAM\QUESTS\quests_reaction.c
case "generate_convoy_quest_completed_2":
    LAi_type_actor_Reset(pchar);                                    // возврат контроля игроку
    LAi_ActorWaitDialog(pchar, characterFromID("quest trader"));    // приказ стоять и ждать диалога
    
    pchar.GenQuest.CantSpeakNPCId_2 = "quest trader";
    LAi_ActorDialog(characterFromID("quest trader"), pchar, "speak_completed_None_2", 2.0, 1.0);    // инициируем диалог
    characters[GetCharacterIndex("quest trader")].dialog.currentnode = "complete_convoy_quest";     // указываем ноду диалога
    pchar.quest.generate_convoy_quest_progress = "completed";       // меняем прогресс выполнения задания
break;
Здесь мы просто инициируем диалог "complete_convoy_quest" с торговцем:
// ..\PROGRAM\DIALOGS\convoy_trader.c
case "complete_convoy_quest":
    dialog.text = "О! Спасибо вам. Под вашей защитой я чувствовал себя как никогда спокойно. Вот ваша награда.";
    Link.l1 = "Благодарю вас.";
    link.l1.go = "exit";
    AddDialogExitQuest("convoy_refused");               // запуск экшена квеста
    OfficersReaction("good");                           // вызываем положительную реакцию офицеров на наши действия
    AddCharacterExpToSkill(pchar, "Sailing", 40);       // добавляем очки в прокачку скиллов
    AddCharacterExpToSkill(pchar, "Leadership", 20);
    AddQuestTemplate("Gen_convoy_quest", "t4");         // добавляем запись в квест бук
    CloseQuestHeader("Gen_convoy_quest");               // отправляем раздел в архив
break;
Здесь тоже всё уже знакомое, выделить можно только строки с реакцией офицеров и прокачкой навыков. Это всё из аддонов на базе ВМЛ. Если у вас в руках одна из оригинальных игр серии - таких записей вы не увидите.
И это, кстати, второй пример плохого кода. Данные строки добавлены разработчиками аддона. Помимо того, что им здесь в принципе не место, они еще и дублируют некоторые действия из кейса "convoy_refused", который вызывается отсюда же.
// ..\PROGRAM\QUESTS\quests_reaction.c
case "convoy_refused":
    pchar.quest.generate_convoy_quest_failed.over = "yes";      // отмечаем все условия как оконченные
    pchar.quest.generate_convoy_quest_completed.over = "yes";
    pchar.quest.generate_convoy_quest_timer.over = "yes";
    
    GetCharacterPos(GetMainCharacter(), &locx, &locy, &locz);
    homelocator = LAi_FindNearestFreeLocator("reload", locx, locy, locz);
    LAi_SetActorType(characterFromID("quest trader"));
    LAi_ActorGoToLocation(characterFromID("quest trader"), "reload", homelocator, "none", "", "", "", 5.0); // торговца отправляем на выход из локации

    if (checkquestattribute("generate_convoy_quest_progress", "completed")) // если задание выполнено
    {
        iPassenger = makeint(pchar.ConvoyQuest.convoymoney);
        AddMoneyToCharacter(pchar, iPassenger);                 // выплачиваем деньги ГГ
        ChangeCharacterReputation(pchar, 1);                    // повышаем репутацию
        OfficersReaction("good");                               // положительная реакция офицеров
        RemoveCharacterCompanion(Pchar, characterFromID("quest trader"));   // удаляем торговца из компаньонов
        pchar.quest.generate_convoy_quest_progress = "";        // обнуляем прогресс задания
    }
    if (!checkquestattribute("generate_convoy_quest_progress", "begin"))    // если прогресс не равен "begin"
    {
        CloseQuestHeader("Gen_convoy_quest");                   // отправляем раздел в архив
    }
    pchar.quest.generate_convoy_quest_progress = "";            // обнуляем прогресс задания
break;
А вот здесь уже интересно. Данный кейс вызывается также при отказе от него в самом первом диалоге с торговцем. Отсюда и проверка на статус "begin" перед отправкой квеста в архив - ведь на тот момент запись в журнале ещё не создана.
А проверка на статус "completed", соответственно, не даёт нам получить награду за выполнение в момент отказа.
Это достаточно простое задание и если разделить кейсы отказа и успешного завершения - то кейс отказа будет полностью дублировать код кейса завершения, только без наград. Поэтому их и объединили. В большинстве же случаев, это реализуется отдельными кейсами.
Нам осталось рассмотреть только условия провала задания.
Если вы помните, их было всего два: таймер и смерть торговца. Есть ещё такой игровой момент как нападение на него, но разработчики подразумевают смерть торговца в итоге такого развития событий.
Начнём с таймера. Освежим в памяти ф-цию его объявления:
SetTimerCondition("generate_convoy_quest_timer", 0, 0, sti(pchar.ConvoyQuest.iDay), false);
Устройство данной ф-ции мы разберём в соответствующем уроке, а здесь нас интересует только первый её аргумент - это кейс в квест-реакшн, который будет вызван по истечению таймера.
// ..\PROGRAM\QUESTS\quests_reaction.c
case "generate_convoy_quest_timer":
    AddQuestTemplate("Gen_convoy_quest", "t2"); // запись в судовой журнал
    CloseQuestHeader("Gen_convoy_quest");       // отправляем раздел журнала с нашим квестом в архив
    
    sld = characterFromID("Quest trader");
    ChangeCharacterHunterScore(GetMainCharacter(), NationShortName(sti(sld.BakNation)) + "hunter", 10+rand(10));    // добавляем чуть монеток в награду за голову ГГ
    RemoveCharacterCompanion(Pchar, sld);       // убираем торговца из компаньонов
    OfficersReaction("bad");                    // отрицательная реакция офицеров
    ChangeCharacterReputation(pchar, -10);      // режем репутацию
    pchar.quest.generate_convoy_quest_progress = "";            // обнуляем прогресс задания
    pchar.quest.generate_convoy_quest_failed.over = "yes";      // отмечаем все условия как оконченные
    pchar.quest.generate_convoy_quest_completed.over = "yes";
break;
По наполнению очень похоже на предыдущий кейс, только наоборот: запись в квестбук о провале, а не завершении, репутация и отношение офицеров - в минус. Ещё и с нацией торговца отношения портятся.
В случае гибели торговца вызываемый кейс был указан следующий:
pchar.quest.generate_convoy_quest_failed.win_condition = "generate_convoy_quest_failed";
Смотрим, что внутри:
// ..\PROGRAM\QUESTS\quests_reaction.c
case "generate_convoy_quest_failed":
    ChangeCharacterReputation(pchar, -5);       // понижение репутации
    OfficersReaction("bad");                    // отрицательная реакция офицеров
    RemoveCharacterCompanion(Pchar, characterFromID("quest trader"));   // убираем торговца из компаньонов
    pchar.quest.generate_convoy_quest_progress = "";            // обнуляем прогресс задания
    pchar.quest.generate_convoy_quest_failed.over = "yes";      // отмечаем все условия как оконченные
    pchar.quest.generate_convoy_quest_completed.over = "yes";
    pchar.quest.generate_convoy_quest_timer.over  = "yes";
    
    AddQuestTemplate("Gen_convoy_quest", "t3"); // запись в судовой журнал
    CloseQuestHeader("Gen_convoy_quest");       // в архив
break;
Собственно, здесь всё то же самое, только штрафа к отношению с нацией нет, так как это не прямая наша вина, что торговец погиб.
Вы скажете, что ведь игрок сам мог его потопить - но при нападении на "своего" игрок и так получает соответствующий штраф.

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

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