Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
683 lines (515 sloc) 63 KB

Руководство разработчика текстовых приключений на основе Quazatron Adventure

0. Вступление

Quazatron Adventure - игровой движок, написанный на языке JavaScript и служащий для создания игр в жанре текстового приключения. Для работы с движком от разработчика требуется понимание основ языка программирования JavaScript.

Давайте рассмотрим возможности Quazatron Adventure на примере создания простой текстовой игры.

1. Готовим файлы index.html и styles.css

Quazatron Adventure (далее QA) представляет собой комплект файлов с кодом на языке JavaScript, в которых содержится вся логика и все игровые данные, но которые сами по себе работать не будут. Перед началом разработки вам нужно продумать, как ваша игра будет взаимодействовать с пользователем.

скачайте все файлы шаблона игры напрямую из этого репозитория или клонируйте: git clone https://github.com/eidolonzx/quazatron-adventure-boilerplate.git

Изначально QA затачивался под его использование в браузере на стороне клиента, хотя с небольшой доработкой может быть использован и на стороне сервера, и в виде настольного приложения (например, с помощью фреймворка Electron). Здесь мы будем рассматривать "классический" вариант работы QA - в браузере на стороне клиента. Это означает, что вам нужно создать html-файл и подключить к нему нужные стили CSS и, разумеется, QA.

В шаблоне Quazatron Adventure Boilerplate в папке src уже есть файлы index.html и styles.css, которые вы можете использовать как образец для создания своей разметки и стилей.

index.html - файл с разметкой всех игровых элементов, которые будут присутствовать на экране пользователя. Именно этот файл нужно запустить, чтобы сыграть в игру.

styles.css - файл со стилями оформления всех игровых элементов.

Придумывая оформление экрана игры, я ориентировался в первую очередь на классические adventure-игры с графикой, например, Kayleth. Сверху располагается экран с картинкой локации, ниже - текст описания локации, ещё ниже выводятся ответы программы на действия пользователя. И в самом низу находится строка для ввода комманд. Вы можете оставить оформление или же переделать под себя, но в этом случае учтите, что для Quazatron Adventure обязательно наличие блоков

со следующими id:

  • <div id="image"></div> - блок для отображения графики. По умолчанию для каждой игровой локации вы должны назначить своё изображение, а если не хотите использовать графику, то эту функцию можно будет отключить (об этом дальше). Если вам не нужна графика для локаций, то уберите или закомментируйте этот блок.
  • <div id="screen"></div> - основной игровой блок, по умолчанию сюда выводится описание локации и реакция программы на действия пользователя.
  • <div id="input-area"><input id="input-field" type="text" autofocus></div> - строка ввода комманд пользователя, стандартный html-элемент input.

В файле styles.css нужно сохранить следующие стили: .action-ask, .inventory-item, .location-item, .encounter. Они задают цвет, соответственно, сообщениям с реакцией программы на ввод пользователя, предметам в инвентаре, предметам в локации и игровым объектам, с которыми игрок может взаимодействовать.

И, наконец, подключение движка к index.html производится следующим способом: <script type="module" src="app.js"></script>

Разместите этот тег перед закрывающим тегом </body>.

2. Сценарий игры

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

Итак, игрок спасается от зомби и попадает на территорию заброшенного завода. Территория огорожена забором, единственный вход - через автоматические ворота, которые на начало игры открыты. Рядом с воротами находится будка охраны. У игрока есть лишь несколько ходов, чтобы разобраться с тем, как закрыть ворота и тем самым спастись от зомби.

Игра будет состоять из трёх локаций. Первая локация - территория снаружи завода, вторая локация - огороженная забором территория завода (а именно место около ворот, рядом с будки охраны), и третья - внутри будки охраны. Чтобы спастись от зомби, игроку нужно забежать на территорию завода, проникнуть в будку охраны и нажать кнопку закрытия ворот. Просто так кнопку нажать не получится - пульт под напряжением, и здесь нам пригодятся найденные ранее резиновые перчатки. Чтобы попасть в будку охраны, нужно сначала открыть её дверь.

3. Создаём игровые локации

Для создания игры вам потребуется изменить только файлы в папке gamedata. Этого достаточно, если вы хотите создать новую игру без вмешательства в движок. Все файлы в данной папке написаны на языке javascript и содержат исходные данные и игровую логику.

Первое, с чего имеет смысл начинать в работе над игрой, это создание локаций. Вся информация о локациях, которые может посетить игрок, задаётся в объекте locations файла locations.js.

Объект locations представляет собой массив объектов JavaScript: [ { локация 1 }, { локация 2 }, ... { локация n }].

Объекты локаций записываются в следующем виде:

{
  id: 0,
  desc: 'Локация #0. Выход - на север в локацию #1',
  dir: {
    n: 1,
  },
  img: 'location000.png',
}
  • id - уникальный цифровой id локации (целое число больше нуля)
  • desc - описание локации, которое выводится на экран игроку
  • img - изображение, соответствующее данной локации
  • dir - объект, содержащий информацию о переходах из локации.

Объект dir построен следующим образом:

dir: {
        n: 2,
        e: -1,
        s: 0,
        w: -1,
        u: -1,
        d: -1,
        ne: -1,
        nw: -1,
        se: 3,
        sw: -1,
    }

Его свойства n, e, s, w, u, d, ne, nw, se, sw содержат номер локации, в которую игрок переходит при перемещении, соответственно, на север, на восток, на юг, на запад, вверх, вниз, на северо-восток, на северо-запад, на юго-восток, на юго-запад. Если переместиться в направлении нельзя, то вместо номера присваивается -1. Для упрощения процесса записи локаций допускается вообще не указывать свойства направлений, в которые перемещение невозможно.

Обратите внимание, что в dir задаются все возможные направления не смотря на то, что в какие-то моменты игры они могут быть недоступны. То есть, если путь на восток теоретически возможен, но его преграждает запертая дверь или монстр, то в свойстве e всё равно указывается номер локации на востоке, а ограничение перемещения нужно будет прописать в encounters (об этом - дальше).

Создадим три локации для нашей мини-игры:

const locations = [{
  id: 0,
  desc: 'Вы находитесь около какого-то завода, на территорию которого можно попасть через !*ворота*! на !*юге*!. Толпа !*зомби*! медленно движется прямо в вашу сторону!',
  dir: {
    s: 1,
  },
  img: 'location000.png',
}, {
  id: 1,
  desc: 'Вы на территории завода. Выход отсюда - через ворота на !*севере*!. На востоке находится небольшая !*будка*!, когда-то выполнявшая, видимо, функции поста охраны.',
  dir: {
    n: 0,
    e: 2,
  },
  img: 'location001.png',
}, {
  id: 2,
  desc: 'Вы внутри будки охраны. Выход отсюда - через !*дверь*! на западе. Здесь нет ни мебели, ни техники, только небольшой !*пульт*! на стене.',
  dir: {
    w: 1,
  },
  img: 'location002.png',
}];

Чтобы в игре отображались картинки, соответствующие каждой локации, вам понадобится их сделать и положить в папку img. Процесс создания графики к игре выходит за рамки данного мануала, и для мини-игры вы можете оставить свойства img такими, как есть, или пустыми.

Обратите внимание - некоторые слова в описаниях локаций оборачиваются в конструкцию !*слово*!. При выводе на экран такие слова будут выделены другим цветом. Как правило, это делается, чтобы показать игроку, с какими объектами он может взаимодействовать, и в каких направлениях двигаться. Давать игроку такую подсказку или нет - решать вам.

4. Создаём словари

Следующий после создания локаций шаг - это создание словарей. В файлах-словарях находятся все слова, которые понимает игра. В QA таких словаря три: файл verbs.js с глаголами, objects.js с существительными и adjectives.js с прилагательными.

Каждый словарь, аналогично локациям, представляет собой массив объектов JavaScript: [ { слово 1 }, { слово 2 }, ... { слово n} ]. Каждому слову соответствует свой цифровой id, а также ряд свойств.

Рассмотрим каждый из словарей подробнее.

4.1 Глаголы - verbs.js

В словарь verbs входят игровые команды-глаголы. По умолчанию каждая команда игрока должна начинаться с глагола или со служебного слова / сокращения (далее все служебные слова и глаголы я буду именовать командами, для простоты).

Команды с id от 0 до 20 зарезервированы программой. Их изъятие из словаря сломает игру.

id Команда
0 с (идти на север)
1 в (идти на восток)
2 ю (идти на юг)
3 з (идти на запад)
4 х (идти наверх)
5 н (идти вниз)
6 св (идти на северо-восток)
7 сз (идти на северо-запад)
8 юв (идти на юго-восток)
9 св (идти на юго-запад)
10 иди
11 инфо (информация об игре)
12 выход (закончить игру)
13 и (инвентарь)
14 сохр (сохранить игру)
15 загр (загрузить игру)
16 отм (отменить ход)
17 п (повторить ход)
18 б (взять)
19 к (положить, класть)
20 о (осмотреть)

Начиная с id=21 вы можете создавать свои команды.

Для удобства разработчика в движке уже заданы самые распространённые для текстовых игр команды, такие как "открыть", "сломать", "ударить", "спросить" и так далее, и большинство вариантов их использования. Вы можете их оставить и добавить нужные вам глаголы, а можете удалить и написать свои с нуля - дело ваше.

Каждая команда (кроме первых 21 служебных) записывается как объект со следующими ключами и значениями:

{
  id: 46,
  forms: ['пить', 'пей', 'выпить', 'выпей', 'попить', 'попей', 'пью', 'выпью'],
  method: 'drink',
}
  • id - уникальный цифровой идентификатор объекта в данном словаре (целое положительное число или 0). Во избежание путаницы и сбоев рекомендуется задавать id как порядковый номер начиная с нуля (для первого объекта id = 0, для следующего id = 1 и так далее).
  • forms - массив, в котором находятся варианты написания команды в виде строк, синонимы. Здесь вы должны постараться учесть все варианты написания команды, которые может использовать игрок. Вы можете прописывать любое количество синонимов как в повелительном наклонении (например, "пей"), так и в неопределённой форме ("пить").
  • method - свойство, которое показывает, какая функция объекта encounters, отвечающего за игровую логику (о нём см. дальше) будет обрабатывать ввод игрока, который ввёл эту команду.

Для создания нашей мини-игры мы не будем изменять словарь verbs, так как имеющихся в нём глаголов будет вполне достаточно для нашего сценария.

4.2 Прилагательные - adjectives.js

В словарь adjectives входят используемые в игре прилагательные.

У каждого прилагательного есть два свойства: его id и свойство forms, массив, в котором находятся все варианты написания прилагательного (аналогично тому, как это сделано у глаголов).

Привязка прилагательных к определённым предметам или игровым объектам производится в словаре объектов objects.js.

Важное замечание! Не следует создавать прилагательные "для красоты", так как работа с прилагательными усложняет ввод для пользователя (ему нужно будет вводить и прилагательное, и существительное, чтобы игра его поняла). Создавайте прилагательные только для тех случаев, когда в игре используется несколько однотипных объектов, различающихся каким-либо свойством (чаще всего цветом): золотая, серебряная и медная монеты; красная, синяя и жёлтая кнопки; круглая и треугольная таблетки и так далее.

В мини-игре мы не будем использовать прилагательные, поэтому пропускаем.

4.3 Предметы и объекты - objects.js

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

Объектом с id = 0 в словаре является абстрактный объект "всё", который нужен для реализации таких команд, как "взять всё" (взять все предметы, лежащие в локации) и "положить всё" (положить все предметы из инвентаря в локацию). Его трогать не следует, а начиная с id = 1 можно создавать свои предметы.

Предметы записываются как объекты JavaScript со следующими свойствами и значениями:

{
  id: 1,
  name: 'лестница',
  forms: ['лестница', 'лестницу', 'лестницой', 'лестнице', 'лест'],
  canHold: true,
  desc: 'Это лестница. Лестница - предмет, её можно взять.',
  adjective: -1,
}

Все остальные объекты или персонажи записываются следующим образом:

{
  id: 3,
  forms: ['тролль', 'тролля', 'троллю', 'троллем', 'тро', 'трол', 'тролл'],
  canHold: false,
  desc: 'Это толстый тролль. Тролль - игровой объект, его нельзя взять, но с ним можно взаимодействовать. По умолчанию он находится в локации 1.',
  location: 1,
  adjective: -1,
}
  • name - строка, выводящаяся как название предмета в инвентаре. Требуется только для предметов, для всех остальных объектов не требуется.
  • forms - массив словоформ, аналогично такому же свойству словарей глаголов и прилагательных. Здесь имеет смысл прописать слово во всех падежах, чтобы предугадать любой вариант ввода пользователя (например, вот так игрок может взаимодействовать с объектом тролль: убей тролля, поговори с троллем, дай денег троллю, спроси о тролле и так далее).
  • canHold - true, если это предмет, который можно поместить в инвентарь; false для всех остальных объектов
  • desc - описание предмета, которое выводится игроку по команде "осмотреть" по умолчанию.
  • location - id локации, в которой находится данный объект. Если объект находится в нескольких локациях, то в данное свойство помещается массив с id локаций, например: location: [0, 1].
  • adjective - в это свойство прописывается id прилагательного, которое будет привязано к этому объекту. Если к объекту не привязано прилагательное, то свойству adjective должно быть присвоено значение -1 (либо это свойство можно вообще не прописывать)

Важно! Во избежание путаницы и сбоев в работе движка следует сначала прописывать предметы, а только после них все остальные объекты!

В мини-игре игрок будет взаимодействовать со следующими объектами: зомби, ворота, пульт, кнопка, будка, дверь. Также в нашей игре будет один предмет - перчатки.

Создаём предметы и объекты в файле objects.js:

const objects = [{
  // Объект "ВСЁ" нужен для того, чтобы корректно работали команды "ВЗЯТЬ ВСЁ" и "ПОЛОЖИТЬ ВСЁ".
  // Не удаляйте его
  id: 0,
  forms: ['все', 'всё'],
  canHold: false,
}, {
  id: 1,
  name: 'перчатки',
  forms: ['перчатки', 'перчаток', 'перчаткам', "перчатка", "перчатку", "перчатке"],
  canHold: true,
  desc: 'Это резиновые перчатки.',
}, {
  id: 2,
  forms: ['зомби', 'зомбей'],
  canHold: false,
  desc: 'Это отвратительные зомби, ходячие мертвецы. Ими движет единственное желание - съесть меня.',
  location: [0],
}, {
  id: 3,
  forms: ['ворота', 'ворот', 'воротам'],
  canHold: false,
  desc: 'Это массивные железные ворота.',
  location: [0, 1],
}, {
  id: 4,
  forms: ['пульт', 'пульта', 'пультом', 'пульту', 'пульте'],
  canHold: false,
  desc: 'Это небольшой пульт, вкрученный в стену. На нём есть !*кнопка*!, а ниже наклейка: "Управление воротами". По пульту пробегают электрические разряды.',
  location: 2,
}, {
  id: 5,
  forms: ['кнопка', 'кнопку', 'кнопке', 'кнопки', 'кнопкой'],
  canHold: false,
  desc: 'Это обычная пластиковая кнопка. Её можно нажать.',
  location: 2,
}, {
  id: 6,
  forms: ['будка', 'будки', 'будке', 'будкой'],
  canHold: false,
  desc: 'Это небольшое кирпичное здание. Скорее всего, здесь раньше сидел диспетчер и следил за теми, кто въезжает на территорию завода, и кто выезжает наружу.',
  location: [1, 2],
}, {
  id: 7,
  forms: ['дверь', 'двери', 'дверью'],
  canHold: false,
  desc: '',
  location: [1, 2],
}];

Обратите внимание! Для объекта с id = 7 (двери) мы не задаём свойство desc, потому что для него нет постоянного описания. Согласно логике нашей игры, эта дверь может быть открытой и закрытой, и вывод описания текущего состояния двери мы предусмотрим далее в объекте encounters (см. пункт 7 "Игровая логика").

5. Задаём переменные и объекты игрового состояния

Файл initial-data.js содержит все данные, которые изменяются в процессе игры, а также их первоначальное состояние.

В нём вы должны задать пять переменных:

const defaultLocation - id стартовой локации

const defaultLocation = 0;

const initialItemPlaces - объект, содержащий данные по начальному местоположению всех предметов. Свойства объекта задаются в виде id предмета: id локации. Если изначально предмет не находится ни в одной из локаций, вместо id локации присваивается -1.

const initialItemPlaces = {
  0: -1, // "Всё" не является предметом как таковым, и ему локация не присваивается
  1: 1, // Перчатки (id = 1) находятся в локации 1
};

const initialFlags - объект, содержащий игровые флаги (триггеры, переключатели). Свойства объекта задаются в виде флаг: начальное состояние (true / false).

Флаги используются в игровой логике в функциях объекта encounters, который вы запрограмируете далее.

Например, нам нужно отслеживать состояние двери: закрыта она или открыта. Для этого создаём флаг isDoorOpened и присваиваем ему начальное значение false (потому что дверь закрыта). После того, как игрок откроет дверь, флаг isDoorOpened поменяется на true.

Для игры зададим ещё несколько флагов:

  • areGlovesWorn - надел ли игрок перчатки?
  • isDiedFromZombies - игрока съели зомби
  • isDiedFromElectricShock - игрока убило током

Последние два флага непосредственно во время игры не применяются, но позволяют отобразить соответствующий причине смерти текст на экране окончания игры. С этим мы разберёмся позже, а пока создайте все эти флаги.

Флаги isVictory и isGameOver из шаблона являются обязательными (означают, соответственно, наступление победы или проигрыша), без них логика игры работать не будет.

const initialFlags = {
  isVictory: false,
  isGameOver: false,
  isDoorOpened: false,
  areGlovesWorn: false,
  isDiedFromZombies: false,
  isDiedFromElectricShock: false,
};

const initialCounters - объект, содержащий игровые счётчики. Счётчики аналогичны флагам, но если флаг принимает булевые значения true / false, то счётчики принимают числовые значения. Счётчики можно использовать, например, для подсчёта игровых очков или ходов, которые сделал игрок. Как и флаги, счётчики используются в объекте encounters.

В мини-игре мы будем использовать счётчик для подсчёта ходов, которые сделал игрок. Если он будет не слишком расторопным и не успеет закрыть вовремя дверь, то зомби его сожрут. Дадим игроку возможность закрыть ворота за 10 игровых ходов, и с каждым ходом будем уменьшать этот счётчик.

const initialCounters = {
  turnsToZombieArrival: 10,
};

const initialInventory - массив, содержащий в себе id всех предметов, которые находятся в инвентаре с начала игры.

В начале мини-игры у игрока нет никаких предметов, поэтому оставляем массив пустым.

const initialInventory = [];

6. Задаём изображения и тексты "по умолчанию"

За изображения и тексты по умолчанию отвечает файл default-data.js, который содержит два объекта: defaultTexts и defaultImages.

В defaultTexts прописываются все текстовые сообщения, не связанные с игровой логикой: тексты стартового экрана, экранов выигрыша и проигрыша; дефолтные ответы на служебные команды, призывы игрока к действию в тех или иных ситуациях и так далее.

В defaultImages указываются изображения, которые следует выводить для стартового экрана, экранов выигрыша и проигрыша.

В defaultTexts уже содержатся все служебные сообщения, а также шаблонная фраза для стартового экрана, экрана победы и экрана проигрыша. Если хотите, вы можете их поменять.

7. Игровая логика

А теперь самое интересное: объект encounters, который находится в файле encounters.js и содержит в себе всю игровую логику. По сравнению со всеми предыдущими объектами и массивами encounters является самым сложным, более того, работа с этим объектом требует от разработчика игры базовых знаний в языке JavaScript, поэтому разберём его детально, а потом перейдём к написанию игровой логики.

Объект encounters содержит ряд дефолтных методов, удаление которых может привести к потере движком работоспособности. Помимо дефолтных методов разработчик добавляет свои методы, соответствующие командам (их название должно совпадать со свойством method в словаре verbs).

Для выполнения тех или иных действий внутри собственных методов объект encounters активно использует методы игровых классов, код которых располагается в папке classes. И, для того, чтобы разобраться с тем, как работает encounters, для начала разберёмся с этими методами.


Методы игровых классов, в которых хранится текущее состояние игры

К игровым классам относится пять классов: Counters, Flags, Inventory, ItemPlaces и CurrentLocation. Эти классы используются для хранения и обработки соответствующих им игровых состояний: счётчиков, флагов, инвентаря, мест расположения предметов и локации, в которой находится игрок. У каждого из игровых классов есть методы для получения или изменения данных, относящихся к классу.

У игровых классов также есть служебные методы (например, .init - инициализация для начала игры), которые используются самим движком. В списках ниже эти методы не приводятся.

1. Counters - работа со счётчиками

  • Counters.get(counterKey) - получить значение счётчика counterKey;
  • Counters.set(counterKey, value) - установить в счётчик counterKey значение value;
  • Counters.increase(counterKey) - увеличить значение счётчика x на 1;
  • Counters.decrease(counterKey) - уменьшить значение счётчика x на 1.

2. Flags - работа с флагами

  • Flags.get(flagKey) - получить значение флага flagKey;
  • Flags.toggle(flagKey) - переключить флаг flagKey (с true на false и наоборот).

3. Inventory - работа с инвентарём

  • Inventory.clear() - очистка инвентаря;
  • Inventory.addItem(itemId) - добавить в инвентарь предмет itemId;
  • Inventory.removeItem(itemId) - убрать из инвентаря предмет itemId;
  • Inventory.includes(itemId) - возвращает true, если предмет itemId есть в инвентаре, иначе false.

4. ItemPlaces - работа с расположением предметов

  • ItemPlaces.set(itemId, locationId) - помещает предмет itemId в локацию locationId;
  • ItemPlaces.get(itemId) - возвращает id локации, в которой находится предмет itemId.

5. CurrentLocation - работа с текущей локацией

  • CurrentLocation.set(locationId) - перемещает игрока в локацию locationId;
  • CurrentLocation.get() - возвращает id текущей локации.

Собственные методы объекта encounters

Помимо операций с игровыми классами объект encounters использует собственные методы для реализации той или иной логики.

addDescription()

Метод служит для добавления к стандартному описанию локаций текста, который зависит от текущего состояния игры. Добавочный текст хранится в переменной encounter. Если добавочный текст выводить не надо, то метод возвращает пустую строку, иначе метод возвращает содержание переменной encounter.

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

if (CurrentLocation.get() === 1) {
  if (Flags.get('isDoorOpened')) encounter = 'Дверь открыта.';
  else encounter = 'Дверь закрыта.'
}

Если игрок находится в локации 1, то добавляем к описанию локации строку про состояние двери. Если флаг isDoorOpened является true, то к описанию добавляется строчка про то, что дверь открыта, иначе - что закрыта.

addInfoToInventory()

Метод служит для добавления дополнительной информации в текст, выводимый по команде "И" ("ИНВЕНТАРЬ"). По умолчанию этот текст хранится в переменной info и пуст ('').

С помощью этого метода мы добавим в инвентарь дополнительную строчку, сообщающую игроку, что он надел перчатки.

if (Flags.get('areGlovesWorn')) info += " Перчатки надеты на руки.";

Если флаг areGlovesWorn является true, то добавляем вывод инвентаря информацию про перчатки.

checkPlayerObstacles(direction)

Метод возвращает текст, который выводится, если игрок пытается пройти в направлении direction, которое по логике игры пока ему недоступно.

В нашей игре это будет случай, когда игрок пытается пройти на восток в будку через закрытую дверь.

Пример:

if (CurrentLocation.get() === 1 && !Flags.get('isDoorOpened') && direction === 'e') return 'Я не могу пройти через закрытую дверь.';

Если игрок находится в локации 1 и флаг isDoorOpened является false и игрок пытается пройти в направлении e - восток - то выдаём ему сообщение о том, что он не может пройти.

getGameOverText()

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

Дадим игроку возможность умереть двумя разными способами (ну и что, что игра маленькая, а что вы хотели от постапокалипсиса?)

if (Flags.get('isDiedFromZombies')) return 'Вы слишком долго искали способ закрыть ворота. Зомби проникли на территорию завода и съели вас!';
if (Flags.get('isDiedFromElectricShock')) return 'Дотрагиваться до наэлектризованной панели голыми руками - не лучшая идея. Вы умерли от удара током!';
return defaultTexts.defaultGameOverText;

Код выше проверяет каждый из "смертельных" флагов и, в зависимости от того, какой из них включен, выдаёт соответствующее сообщение. Если нет конкретной причины смерти (например, когда игрок не умер, а просто решил выйти из игры по команде "Выход"), то выдаётся текст экрана окончания игры, заданный по умолчанию. Сами "смертельные" флаги мы будем активировать чуть дальше.

setCounters()

Метод вызывается на каждом игровом ходу после обработки команды игрока и позволяет "прокручивать" значения счётчиков или выполнять некие действия каждый ход (если это вам нужно, а если не нужно - оставьте метод setCounters пустым.

Мы используем его для обратного отсчёта ходов до того момента, когда зомби доберутся до игрока.

Counters.decrease('turnsToZombieArrival');
if (Counters.get('turnsToZombieArrival') === 0) {
  Flags.toggle('isGameOver');
  Flags.toggle('isDiedFromZombies');
}

Каждый ход уменьшаем счётчик turnsToZombieArrival на 1. Если счётчик достиг нуля, то включаем два флага: флаг isGameOver, который даёт движку понять, что игра закончилась, и флаг isDiedFromZombies, который позволяет вывести сообщение о том, что игрок умер от зомби. Из-за того, что флаг isGameOver включён, игра прерывается, и игрока перекидывает на экран окончания игры.

getUniqueEncounter()

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

В демо-игре "Спящая красавица" метод getUniqueEncounter() используется, чтобы выкидывать игрока из комнаты с ведьмой, когда тот совершает неправильное действие:

getUniqueEncounter(verbId, objectIds) {
    let answer, flag = false;

    if (CurrentLocation.get() === 27 && !Flags.get("isWitchKilled")) {
        flag = true;
        if (verbId === 32 && objectIds.includes(25)) {
            if (Inventory.includes(10)) {
                Flags.toggle("isWitchKilled");
                Inventory.removeItem(10);
                answer = "Вы отразили заклятье мечом, и оно ударило прямо в ведьму! Издав истошный крик, ведьма рассыпалась в пыль. К сожалению, меч тоже не уцелел.";
            } else {
                CurrentLocation.set(17);
                answer = "Вам нечем отразить заклятье. Заклятье ударяет вам в грудь и выкидывает отсюда в комнату с решёткой."
            }
        } else {
            CurrentLocation.set(17);
            answer = "Вы не успеваете ничего сделать. Заклятье ударяет вам в грудь и выкидывает отсюда в комнату с решёткой.";
        }
    }

    return {
        answer,
        flag
    }
}

Если игрок находится в локации id = 27, и ведьма ещё жива, то внутренний флаг функции flag устанавливается в true. Далее происходит проверка условий, благодаря которым игрок может убить ведьму (правильная команда и присутствие меча в инвентаре). После этого метод возвращает ответ, который выводится игроку, а также внутренний флаг. Данный флаг показывает движку, что следует вернуться к следующему игровому циклу, игнорируя обработку любых команд. Благодаря этому игрок не может выполнить ни одного действия, кроме единственно верного.

В нашей мини-игре мы не будем использовать getUniqueEncounter() ввиду его сложности и отсутствия в нём необходимости.

take(itemId) и drop(itemId)

Данные методы служат для дополнения стандартной функциональности глаголов "взять" и "положить".

Метод строится на основе условий if, и если ни одно из условий не выполняется, то возвращает дефолтный ответ.

В оба метода также встроена обработка команд "ВЗЯТЬ ВСЁ" и "ПОЛОЖИТЬ ВСЁ".

Давайте встроим в метод drop(itemId) перед последним return такое условие: игрок не может положить (выбросить) перчатки, если он их надел. Чтобы выбросить, ему придётся сначала снять перчатки.

if (Inventory.includes(1) && objectIds.includes(1) && Flags.get('areGlovesWorn')) return 'Сначала снимите перчатки.';

Если в инвентаре есть перчатки (id = 1) и игрок ввёл команду "ПОЛОЖИТЬ ПЕРЧАТКИ" и флаг areGlovesWorn равен true, то сообщаем игроку, что он должен сначала снять перчатки.

examine(id)

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

Для вывода описания объекта, которое занесено в словарь objects в свойство desc объекта, отдельной логики прописывать не нужно. Данный метод используется только тогда, когда игровой объект в ходе игры как-либо меняется, и в различных ситуациях нужно выводить различные описания.

В мини-игре мы дадим игроку возможность осмотреть дверь в будку охраны и выяснить, открыта она или закрыта.

const currentLocation = CurrentLocation.get();

if ((currentLocation === 1 || currentLocation === 2) && objectId === 7) {
  if (Flags.get('isDoorOpened')) return 'Это ржавая металлическая дверь. Сейчас она открыта.';
  else return 'Это ржавая металлическая дверь. Сейчас она закрыта.';
};

Если игрок находится в локации 1 или локации 2 и при этом хочет осмотреть предмет с id = 7 (это дверь), то игра выдаёт ему описание двери в зависимости от состояния флага isDoorOpened.

Примечание Использование описаний объектов внутри метода examine объекта encounters (вариант 1) перекликается с использованием свойства desc в словаре с объектами (вариант 2). Вариант 1 имеет всегда высший приоритет, нежели вариант 2. То есть, в свойстве desc объекта с id = 7 (дверь) в словаре объектов мы могли ничего не писать, оставив пустую строку (см. пункт 4).

Методы пользовательских команд

Для каждой команды, кроме зарезервированных (см. пункт 4.1 инструкции), вам как разработчику требуется создать отдельный метод в объекте encounters и прописать его название в словаре verbs в свойстве method глагола.

По умолчанию в словаре глаголов verbs.js уже прописаны многие часто встречающиеся в текстовых играх команды. Даже если вы не хотите их использовать в игре, игра будет понимать их, реагируя стандартным сообщением. Попробуйте, например, ввести команду "ЖДИ" или "ПОЙ". Эти глаголы никто не запрещает вам удалить и прописать вместо них свои, главное, следите, чтобы в словаре глаголов были указаны правильные методы.

Общий принцип работы методов пользовательских команд похож на работу метода examine() за исключением того, что в них передаётся не id объекта, а массив id объектов, который упомянул в своём вводе игрок, и, если это нужно, количество слов, которые ввёл игрок.

В общем случае пользовательская команда выглядит так:

command(objectIds, objectsInInput) {
    /*
    Тут различная логика
    */

    return 'Тут ответ по умолчанию';
  }

Зачем нужно передавать количество введённых объектов? Это может понадобится для реализации некоторых команд, которые не могут работать с одним объектом. Например, это такие команды, как "ПРИСЛОНИ" или "ПРИВЯЖИ". Команды "ПРИСЛОНИ ЛЕСТНИЦУ" или "ПРИВЯЖИ ВЕРЁВКУ" непонятны, потому что нужно знать, к чему нужно прислонить лестницу или привязать верёвку. Соответственно, вы как разработчик можете реализовать проверку количества введённых объектов и, в случае, когда введён один объект, запросить у игрока уточнение: "Уточните, к чему вы хотите прислонить лестницу"?

objectIds содержит массив id объектов, которые ввёл игрок. Проверить, есть ли в вводе игрока тот или иной объект, можно с помощью встроенного в JavaScript метода .includes().

Использование массива позволяет обрабатывать команды с разным порядком существительных. В этом случае команда "Купи у торговца сыр" и "Купи сыр у торговца" выполнится одинаково успешно.

В нашей мини-игре помимо стандартных "взять", "положить" и "осмотреть" мы реализуем ещё пять команд:

  1. Игрок сможет открыть дверь в будку
  2. Игрок сможет закрыть дверь в будку.
  3. Игрок сможет одеть перчатки, которые найдёт в локации 1.
  4. Если перчатки одеты, игрок сможет их снять.
  5. Игрок сможет нажать на кнопку, которую он обнаружит на пульте в локации 2.

Команды "ОТКРЫТЬ", "ЗАКРЫТЬ", "НАДЕТЬ", "СНЯТЬ" и "НАЖАТЬ" уже есть среди глаголов по-умолчанию, и нам останется лишь прописать код внутри их методов.

ОТКРЫТЬ

open(objectIds) {
  const currentLocation = CurrentLocation.get();
  if (objectIds.includes(7) && (currentLocation === 1 || currentLocation === 2)) {
    if (Flags.get('isDoorOpened')) {
      return 'Дверь уже открыта.';
    } else {
      Flags.toggle('isDoorOpened');
      return 'Вы открыли дверь.';
    }
  }
  return 'Тут нечего открывать.';
},

Если игрок находится в локации 1 или 2, и даёт команду открыть дверь, то проверяем состояние флага isDoorOpened. Если true, то дверь открыта, и её не нужно открывать. Если false, то дверь закрыта, и игрок открывает дверь.

ЗАКРЫТЬ

close(objectIds) {
    const currentLocation = CurrentLocation.get();
    if (objectIds.includes(7) && (currentLocation === 1 || currentLocation === 2)) {
      if (!Flags.get('isDoorOpened')) {
        return 'Дверь уже закрыта.';
      } else {
        Flags.toggle('isDoorOpened');
        return 'Вы закрыли дверь.';
      }
    }
    if (objectIds.includes(3) && (currentLocation === 0 || currentLocation === 1)) return 'Ворота слишком тяжёлые, чтобы можно было закрыть их руками. Нужно найти другой способ.'

    return 'Здесь нечего закрывать.';
  },

Если игрок находится в локации 1 или 2, и даёт команду закрыть дверь, то проверяем состояние флага isDoorOpened. Если true, то дверь открыта, и игрок её закрывает. Если false, то дверь уже закрыта.

Также предусмотрим реакцию на команду "ЗАКРЫТЬ ВОРОТА", если игрок находится в локации 0 или 1. Ворота можно закрыть с пульта, но нельзя закрыть вручную.

НАДЕТЬ

dress(objectIds) {
  if (Inventory.includes(1) && objectIds.includes(1)) {
    if (Flags.get('areGlovesWorn')) return 'Вы уже надели перчатки.';
    else {
      Flags.toggle('areGlovesWorn');
      return 'Я надел перчатки.';
    }
  };
  return 'Среди вещей у меня нет ничего, что я бы мог надеть. Впрочем, моя обычная одежда мне и так нравится.';
},

Если в инвентаре есть перчатки (id = 1) и игрок ввёл команду "НАДЕТЬ ПЕРЧАТКИ", переходим в блок обработки ситуации с перчатками. В ином случае выводим сообщение, что игроку нечего одеть.

В блоке проверяем флаг areGlovesWorn, если он true, то перчатки уже надеты, выводим соответствующее сообщение. В ином случае переключаем флаг areGlovesWorn с false на true и сообщаем, что одели перчатки.

СНЯТЬ

undress(objectIds) {
  if (Inventory.includes(1) && objectIds.includes(1)) {
    if (!Flags.get('areGlovesWorn')) return 'Вы не одевали перчатки.';
    else {
      Flags.toggle('areGlovesWorn');
      return 'Я снял перчатки.';
    }
  };
  return 'Я могу снять с себя только мою одежду. Но сейчас не та ситуация, когда имеет смысл раздеваться.';
},

Здесь логика аналогичная методу dress(). Мы проверяем флаг areGlovesWorn, если он false, то перчатки нельзя снять, потому что они не надеты, выводим соответствующее сообщение. В ином случае переключаем флаг areGlovesWorn с true на false и сообщаем, что сняли перчатки.

НАЖАТЬ

press(objectIds) {
  if (CurrentLocation.get() === 2 && objectIds.includes(5)) {
    if (Flags.get('areGlovesWorn')) {
      Flags.toggle('isVictory');
      return '';
    } else {
      Flags.toggle('isGameOver');
      Flags.toggle('isDiedFromElectricShock');
      return '';
    }
  }
  return 'Это бесполезно.';
},

Забавно, но нажатие кнопки в этой мини-игре приводит нас или к победе, или к поражению, в зависимости от того, надели ли мы перчатки.

Если мы находимся в локации 2, и игрок отдал команду "НАЖАТЬ КНОПКУ"(обратите внимание, что самой кнопки нет в описании локации, и игрок должен предварительно осмотреть пульт), то проверяем, надел ли игрок перчатки.

Если надел, то он выигрывает игру. Выставляем флаг isVictory, который перекидывает его на победный экран.

Если не надел, то его убивает током. Выставляем флаг isDiedFromElectricShock, определяющий, что игрок умер от поражения током, и выставляем флаг isGameOver, перекидывающий на экран проигрыша.

8. Дополнительные настройки

В файле modules/constants.js вы найдёте две глобальные константы: DEVELOPER_MODE и TEXT_ONLY_MODE. По умолчанию они выключены (false).

Включив DEVELOPER_MODE, вы сможете получать в консоль браузера сообщения об ошибках, которые могут возникнуть при проектировании игры. Обязательно выключайте её перед релизом игры.

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

9. Что дальше?

Данной инструкции должно хватить для создания простейшей игры. В дальнейшем я дополню инструкцию информацией о логике работы парсера и другими полезными штуками.

You can’t perform that action at this time.