protoparser.js

Руководство автора

Версия 8

СОДЕРЖАНИЕ

  1. Преамбула
  2. Объекты и их свойства
    2.1 Свойство spec
  3. Приступая к созданию игры
  4. Объект game
    4.1 Стандартные сообщения системы
    4.2 Интерфейс командной строки
    4.3 Горячие клавиши
    4.4 История команд
  5. Объект player
    5.1 Свойство nam
    5.2 Свойство desc
    5.3 Свойство loc
    5.4 Свойство hidden
    5.5 Инвентарь
    5.6 Свойство maxCarried
  6. Объект типа room
    6.1 Свойства-направления
    6.2 Свойство visits
    6.3 Свойство head
  7. Объект типа thing
    7.1 Свойство gend
    7.2 Свойство edible
    7.3 Свойство takeable
    7.4 Размещение предмета в нескольких локациях
    7.5 Свойство text
    7.6 Свойство worn
    7.7 Свойства door, closed, locked
    7.8 Свойство moved
    7.9 Свойство examined
    7.10 Свойство hiddenPossession
    7.11 Свойство sceneDesc
  8. Обработка команд
  9. Методы объектов
  10. Объект globalVerbs
  11. Объект events
  12. Продолжение «Фантазии»
    12.1 Ключи
    12.2 Ограничение передвижения
    12.3 Изменение локаций
    12.4 Персонажи
    12.5 Диалоги
    12.6 «Виртуальные» двери и «мнимые» выходы
    12.7 Строка состояния
    12.8 Игровой счет
    12.9 Завершение игры
  13. Сохранение и загрузка
  14. Тестирование и отладка
  15. Настройка стилей элементов игрового окна
  16. Форматирование вывода
  17. Мультимедийные элементы и дополнительные библиотеки
  18. Создание новых и изменение параметров стандартных команд. Виртуальные функции
  19. Сниппеты
    19.1 Все или ничего?

Глава 1

Преамбула

Protoparser.js – легковесный веб-движок (библиотека) для интерактивных текстовых игр с вводом команд («парсеров»).

Приставка «прото» в названии движка, в переводе с древнегреческого, означает «первый». Интерфейс командной строки протопарсера оперирует всего двумя понятиями: ГЛАГОЛ и СУЩЕСТВИТЕЛЬНОЕ. В этом он похож на первые текстовые игры-приключения. Вот основные особенности и возможности протопарсера:

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

Работа над протопарсером началась 17 февраля 2018, а пару месяцев спустя вышла первая версия движка – 1(81). Это руководство описывает версию 8.

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

  1. Курсивом отмечен текст, на который сделан особый акцент.
  2. ЗАГЛАВНЫМИ БУКВАМИ выделены команды, которые вводит пользователь в командную строку интерпретатора.
  3. Моноширным шрифтом выделены фрагменты кода, имена переменных, файлов, и т.п.

Глава 2

Объекты и их свойства

Создавая свою игру, вам предстоит иметь дело с объектами. Объект, говоря по-простому, – это набор пар «свойство-значение». Таких пар у объекта может быть сколь угодно много. В основном, вы будете использовать стандартные свойства объектов. Но, кроме того, вы можете добавлять и свои свойства. Вообще, протопарсер работает с 6 классами объектов («стандартные объекты»): game, player, globalVerbs, events, room и thing. Первые четыре класса представлены единственным экземпляром – самими собой. Последние два – представляют класс объектов, каждый со своим собственным идентификатором (именем). Каждый из этих классов будет рассмотрен позднее в соответствующих главах, а пока мы познакомится с общими принципами работы с объектами.

Все объекты, как уже было сказано выше, представляют собой пары «свойство-значение». Объект может обладать произвольным числом таких пар, в т.ч. не обладать ни одной парой вовсе (пустой объект).

Когда мы создаем объект, то сразу присваивания его переменной.

var myObject = {} // пустой объект с именем myObject

Имя объекта является его идентификатором, именно по этому имени мы будем обращаться к свойствам данного объекта (хотя, есть и другие способы это сделать).

Добавим объекту myObject свойства:

var myObject = {
    color: 'зеленый',
    big: true,
    numbers: ['один', 'два', 'три'],
    action: function() {},
    'some property': 5
}

Свойство и его значение разделяются двоеточием, пары «свойство-значение» – запятыми. Если такая пара последняя в объекте запятую после нее ставить не нужно.

Значением свойства объекта может быть число, строка, массив логическое значение и функция.

Если типом значения является строка она записывается в кавычках. Допустимы как одинарные, так и двойные кавычки.

color: 'зеленый',
name: "Майкл Робертс"

Если свойство содержит сразу несколько значений (массив) они записываются через запятую внутри квадратных скобок.

colors: ['красный', 'желтый', 'зеленый'],
numbers: [1, 2, 3]

Свойство может принимать логическое значение (истина/ложь):

big: true // истина
small: false // ложь

Обратите внимание, мы добавили в наш код комментарии. Текст, начинающийся с // «невидим» для интерпретатора до конца строки. Вы также можете использовать многострочные комментарии:

/*
    очень
    большой
    комментарий
*/

Если имя свойства состоит из более чем одного слова, то оно записывается в кавычках:

'some property': 5

Если значением свойства является функция, например:

action: function() {
    // код функции
}

, то говорят, что объект имеет метод. В данном случае, у нашего объекта есть метод action. Подробнее о методах будет рассказано в главе «Методы объектов».

Вам не обязательно записывать каждое свойство с новой строки. Допустима, например, такая форма записи:

var myObject = {
    numbers: ['один', 'два', 'три'],
    'some property': 5,
    color: 'зеленый', big: true, size: 'small'
}

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

Во время игры протопарсер постоянно обращается к свойствам объектов. Как же узнать свойство объекта? Очень просто.

myObject.big // => true
myObject.color // => зеленый
myObject.numbers[1] // => два

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

Добавлять новые свойства очень просто. Добавим объекту myObject свойство newProperty и сразу инициализируем его значением true:

myObject.newProperty = true

Так же легко ненужное свойство можно удалить:

delete myObject.newProperty

При попытке обратиться к несуществующему свойству интерпретатор выдаст undefined.

myObject.newProperty // => undefined

Однако, попытка вызвать несуществующий метод приведет к ошибке.

Вы можете создать и использовать переменные и вне объектов (в глобальном пространстве имен):

var independentVariable = 100

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

2.1 Свойство spec

Прежде чем мы начнем подробно рассматривать каждый класс объектов необходимо рассказать о свойстве spec. Все объекты, обладающие данным свойством, считаются «стандартными». Объектам game, player, lobalVerbs, events движок автоматически добавляет свойство spec при загрузке игры. Это означает, что вам не нужно добавлять его в свой код. Однако! Если ваш объект является комнатой (локацией) или предметом вы должны добавить ему свойство spec. Если наш объект – предмет, мы указываем spec: 'thing', если комната (локация) – spec: 'room'.

Глава 3

Приступая к созданию игры

Одним из главных преимуществ протопарсера является то, что для написания игры вам не обязательно иметь доступ к какой-то определенной ОС, устанавливать дополнительные программы. Вам, также, не стоит беспокоиться насчет IDE. Достаточно иметь на своем устройстве браузер с поддержкой JavaScript и любую программу, в которой можно набрать и сохранить текст.

В папке protoparser уже лежит готовый шаблон для вашей первой игры. Он называется story.js. Ваша игра может иметь какое-то другое название, но имя файла story.js не должно меняться. В этом руководстве мы, шаг за шагом, будем создавать нашу первую игру. В конце руководства в Приложении 3 есть полный исходный код этой игры, а в папке examples/Fantasia уже лежит готовый файл c игрой story.js. Вы можете запустить файл index.html, чтобы посмотреть, как выглядит готовая игра.

Во время написания игры удобно держать текстовый редактор и браузер открытыми, время от времени переключаясь между ними. После того, как вы сохраните изменения в редакторе просто обновите окно с игрой в браузере, чтобы проверить как все работает. Если ваш текстовый редактор пишет, что не может открыть файл, выберите «Открыть как текст».

Возможно, вы захотите писать игру прямо в браузере, перейдя в т.н. «режим разработчика». Работать в нем не сложно, но из-за обилия вкладок, поначалу, может быть непривычно. Вы можете писать игры в привычном для вас редакторе, а переходить в «режим разработчика» только для отладки. О работе в этом режиме будет рассказано в главах Сохранение и загрузка и Тестирование и отладка.

Глава 4

Объект game

В протопарсере настройки игры представляют собой объект game и располагаются в файле story.js, как и любые другие игровые объекты. Отсутствие данного объекта в story.js приведет к ошибке.

Создадим объект game:

var game = {}

Театр, как известно, начинается с вешалки, а игра с названия. Давайте назовем нашу игру «Фантазия»:

title: 'Фантазия'

Теперь, добавим автора (не забудьте поставить свое имя):

author: 'Алексей Галкин <johnbrown>'

Поскольку тэги (символы < и >) никак не отображаются в окне браузера мы заменили их специальными символами &lt; и &gt;.

Добавим год публикации, лицензию и версию игры:

year: 2018,
license: 'MIT',
version: '8.0'

Осталось добавить информацию с описанием нашей игры:

info: 'Небольшая демонстрационная игра на движке protoparser.js'

Итак, мы создали наш первый игровой объект.

var game = {
    title: 'Фантазия',
    author: 'Алексей Галкин &lt;johnbrown&gt;',
    year: 2018,
    license: 'MIT',
    version: '8.0',
    info: 'Небольшая демонстрационная игра на движке protoparser.js'
}

Вы можете добавлять в объект game только те свойства, которые считаете нужными. Мы могли бы не указывать версию игры или даже название. Соответствующие поля бы просто не отображались в заголовке игры. Однако, хотя, с технической точки зрения, свойство game.title не обязательно рекомендуется его добавить для избежания возможных проблем с загрузкой и сохранением (при наличии нескольких безымянных игр).

4.1 Стандартные сообщения системы

Объект defSysVal содержит целый блок свойств в которых хранятся стандартные значения системы. Например, сообщения о том, что игра загружена или команда непонятна. Полный список всех стандартных свойств со значениями по-умолчанию находится в Приложении 2. Самое замечательное в этих сообщениях то, что вы с легкостью можете их переопределить. Например, если игру не удается сохранить, движок, по-умолчанию, выдает стандартное сообщение:

Ошибка сохранения.

Сделаем это сообщение немного информативнее, добавив в объект game свойство notSavedMsg:

notSavedMsg: 'Ошибка сохранения. Игра не сохранена.'

Если, вдруг, в какой-то момент игры вы захотите вернуть стандартное сообщение сделать это можно будет с помощью инструкции game.[свойство] = defSysVal.[свойство].

Забегая вперед, добавлю, что в процессе написания игры вам может понадобиться заменить выводимый стандартной командой текст на свой. Вместо того, чтобы переопределять стандартные функции-обработчики достаточно добавить в объект game свойство с именем требуемой функции, а в качестве значения ввести текст сообщения.

// Можно переопределить стандартную функцию
function overJump() {
    t.print('Это не приблизит вас к цели.')  
}
// А можно поступить проще
var game = {
   // свойство 1
   overJump: 'Это не приблизит вас к цели.'
   // свойство n
}

Название свойства для добавления в объект game обычно соответствует названию команды-функции, и указано в колонке «Имя метода-перехватчика / имя свойства в объектах gameCommands и game» Приложения 1.

Обратите внимание, свойства overJump нет в списке стандартных свойств объектов, поскольку текст сообщения генерировался непосредственно в функции overJump. Аналогичным образом можно создавать одноименные свойства объекта game для любых игровых- и метакоманд.

Таким образом, переопределение стандартных сообщений в объекте game представляется более простым способом настроить поведение команды в своей игре, чем написание соответствующего метода-перехватчика, но только в том случае, если ваша цель – заменить выводимый командой текст на свой, не изменяя поведения самой функции.

4.2 Интерфейс командной строки

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

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

Аналогично тому, как мы меняли значения стандартных сообщений системы мы можем устанавливать требуемый вид элементов командной строки. По-умолчанию, поле ввода пусто, но вы можете сделать, чтобы в нем все время или при наступлении определенных условий отображался какой-то текст, как если бы он был введен пользователем. При этом, пользователь имеет возможность выполнить предложенную команду, нажав клавишу «Enter» или отредактировать ее.

commandTemplate: 'готовая команда'

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

Символ «>» – «приглашение командной строки» отображается слева от поля ввода. При желании, вы можете заменить символьное представление этого элемента.

prompt: '=> '

Более того, присваивая game.prompt значения игровых переменных, вы можете организовать в командной строке статус-бар – строку, содержащую информацию о состоянии персонажа, игры, мира. Например, номер текущего хода, число заработанных очков, здоровье персонажа и т.п. О том, как это сделать будет рассказано в разделе 12.7 «Строка состояния».

Если вы хотите, чтобы какой-то элемент вовсе не выводился на экран присвойте ему пустое значение.

prompt: ''

Теперь символ «>» не будет выводиться в командной строке.

4.3 Горячие клавиши

В протопарсере реализована система т.н. «горячих клавиш» (хоткеев): ввод команды нажатием одной клавиши. Наиболее часто используемые команды уже имеют свои горячие клавиши (см. Приложение 1).

Вы можете менять стандартное поведение горячих клавиш, назначая на них свои команды. Представим, что в нашей игре у персонажа есть карта, в которую приходится постоянно заглядывать. Облегчим жизнь игроку, «повесив» команду ОСМОТРЕТЬ КАРТУ на клавишу «0» цифровой клавиатуры.

num0Key: 'осмотреть карту'

Если вы меняете назначения горячих клавиш в своей игре крайне желательно указать об этом в игровой справке.

Не все клавиши можно использовать в качестве хоткеев. Свойства, соответствующие доступным горячим клавишам перечислены в Приложении 2. Каждое свойство может содержать только одну команду.

У объекта game есть и другие свойства о которых будет рассказано в следующих главах. Вы также можете обратиться к Приложению 2. «Стандартные свойства объектов» в котором перечислены вообще все стандартные свойства игровых объектов.

4.4 История команд

Всякий раз, когда игрок вводит команду протопарсер анализирует ее, после чего, при определенных условиях, команда попадает в «историю команд».

История команд – это массив game.commandHistory, содержащий команды которые вводил пользователь. По-умолчанию, в массиве может храниться до 10 команд, однако автор игры может установить другое число, изменив значение свойства game.commandHistoryLength. История сохраняется даже после повторной загрузки игры.

C помощью клавиш «стрелка вверх» и «стрелка вниз» можно быстро добавить в строку ввода недавнюю команду. При нажатии на стрелку вверх команды добавляются в строку ввода последовательно: от последней введенной к более ранней. При нажатии на стрелку вниз добавление команд происходит в обратном порядке. Выбранная команда не выполняется автоматически, при желании, игрок может отредактировать ее.

Не все команды, которые вводит пользователь попадают в историю команд. Во-первых, в истории не может быть двух одинаковых команд. Во-вторых, парсер должен полностью распознать команду. В-третьих, объект для действия должен присутствовать в локации. Кроме того, в историю не попадают команды ИСТОРИЯ и ПОВТОРИТЬ. Если в истории уже нет места для новой команды, то самая ранняя удаляется, освобождая место для новой. Игрок может посмотреть все сохраненные команды с помощью команды ИСТОРИЯ.

Автор может отключить историю команд, установив значение game.commandHistoryLength равным нулю.

У пользователя есть возможность автоматически выполнить последнюю команду, сохраненную в истории команд. Для этого существуют команда ПОВТОРИТЬ или П. Стоит заметить, что если автор отключил в своей игре ведение истории, то данная функция будет недоступна.

Глава 5

Объект player

В игре персонаж пользователя представлен специальным объектом – player. Этот объект очень похож на объект типа thing (мы познакомимся с ним позже), однако имеется и ряд отличий. Объект player, как и объект game обязательно должен присутствовать в игре, поэтому давайте сразу его создадим.

var player = {
    nam: ['вы', 'себя', 'себе', 'себя', 'собой', 'себе'],
    desc: 'Вы - обычный турист, неизвестно как попавший сюда.',
    hidden: true,
    loc: 'hall'
}

Все свойства, которые мы добавили объекту player присутствуют и у предметов (thing), а некоторые – и у комнат (room). Давайте подробно рассмотрим каждое из этих свойств.

5.1 Свойство nam

Свойство nam представляет собой массив – упорядоченную последовательность элементов, в данном случае, имен объекта.

Первые шесть элементов nam – склонения объекта по падежам (и.п., р.п., д.п. в.п., т.п., п.п.). Эти значения используются при выводе имени объекта. За ними могут следовать произвольное количество имен-синонимов.

Следите, чтобы у объектов не было одинаковых идентификаторов и элементов nam. Каждый элемент должен состоять строго из одного слова.

5.2 Свойство desc

В значении свойства desc хранится описание объекта. Если вы введете команду ОСМОТРЕТЬ СЕБЯ, то игра выдаст «Вы - обычный турист, неизвестно как попавший сюда.» Если вы введете ОСМОТРЕТЬ <ПРЕДМЕТ>, то получите значение свойства desc этого предмета. Это свойство присутствует и у объектов типа room и содержит, как не сложно догадаться, описание локации.

5.3 Свойство loc

Свойство loc определяет локацию, в которой находится предмет. В нашем случае, это свойство содержит значение hall – идентификатор соответствующей локации. Таким образом, игрок начнет свое приключение именно в этом локации.

5.4 Свойство hidden

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

> осмотреться
Вы стоите в маленькой комнате с окном.

Здесь есть кровать, молоток и телефон.

Обычно, если мы уже описали объект в свойстве desc локации нам не нужно, чтобы имя объекта выводилось повторно. Чтобы скрыть имя объекта, находящегося в локации, применяется свойство hidden. Использование этого свойства не приведет к тому, что объект станет недоступным или мы не сможем выполнить с ним какие-либо действия. Единственное его назначение – не отображать объект в описании локации.

Объект player, по-умолчанию, отображается в локации, как телефон и кровать в нашем примере. Если мы «забудем» добавить ему свойство hidden, то в предыдущем примере результат будет следующий:

Здесь есть вы, кровать, молоток и телефон.

Скроем упоминание о персонаже игрока («вы»).

hidden: true

Кроме имени, свойство hidden скрывает описание объекта в локации (свойство sceneDesc).

Если фраза «Здесь есть: ...» кажется вам не очень художественной вы можете генерировать собственные описания с помощью методов before All и afterAll объекта events. Об использовании этих методов будет рассказано в главе 11.

5.5 Инвентарь

В протопарсере реализована система инвентаря. Однако, объект player, как и прочие объекты, не может непосредственно содержать в себе другие объекты. Механизм «принадлежности» одного объекта другому реализуется через свойство loc объекта. Значением loc объекта player является идентификатор стартовой локации. Соответственно, значением loc предмета является либо значение идентификатора локации, в которой он находится, либо значение 'player'. В последнем случае это и будет означать, что объект находится в инвентаре.

Если вы хотите, чтобы игрок начал свое приключение не с пустыми руками, установите свойство loc: 'player' предметам, которыми вы собираетесь снарядить игрока.

Надетые предметы тоже считаются частью инвентаря и отображаются в нем как «надетые».

Иногда вам может потребоваться узнать сколько предметов несет игрок. Сделать это можно с помощью функции getObjByKV:

getObjByKV('loc','player') // => массив объектов, свойство loc которых равно player

Функция принимает два аргумента: свойство объекта и его значение, и создает «фильтр» или массив объектов, удовлетворяющих нашему запросу.

В нашем примере функция вернет массив всех объектов в игре свойство loc которых равно 'player', т.е. тех, которые находятся в инвентаре. Отлично, скажите вы, но как нам узнать их количество? С помощью метода length:

getObjByKV('loc','player').length // => количество предметов в инвентаре

5.6 Свойство maxCarried

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

Свойство maxCarried устанавливает максимальное число предметов, которое может быть в инвентаре. Добавьте в объект player строку:

maxCarried: 3 // не больше трех предметов

Теперь, при попытке взять предмет, если игрок уже несет три предмета (включая надетые), игра выдаст сообщение «Вы несете слишком много вещей.»

Вы в любой момент можете изменить maxCarried. Предположим, у игрока появился рюкзак. Добавим ему немного «свободного места»:

player.maxCarried = 10 // теперь лимит 10

Свойство maxCarried может быть только у объекта player.

Глава 6

Объект типа room

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

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

var hall = {
    spec: 'room',
    head: 'Длинный коридор',
    desc: 'Серые каменные стены коридора, кажется, покрыты пылью многих веков. 
    На севере расположена невысокая дверь. Каменная спиральная лестница
    поднимается высоко вверх.',
    n: 'round',
    u: 'tower'
}

Кое-что, нам уже знакомо из предыдущих глав (свойства spec и desc). Рассмотрим типичные свойства объектов типа room.

6.1 Свойства-направления

Объекты типа room могут соединяться друг с другом через выходы. Сейчас из нашей локации ведут два выхода: на север в локацию round (свойство n) и вверх в локацию tower (свойство u). Мы обозначили выходы, но не создали сами локации. Сделаем это:

var round = {
    spec: 'room',
    head: 'Круглая комната',
    desc: 'Большая круглая комната выглядит пустой и необжитой. Кажется, хозяин
    (или, быть может, архитектор) поленился придать ей индивидуальность. 
    Единственное украшение комнаты, не считая вас, - деревянная дверь на юге.'
}

var tower = {
    spec: 'room',
    head: 'Башня',
    desc: 'Открытая площадка башни позволяет видеть округу на много километров.
    В отдалении на севере блестит небольшое озеро и виднеется выход из парка.
    Вниз ведет лестница.'
}

Кажется, в наших новых локациях чего-то не хватает. Мы «забыли» добавить в них выходы. Если игрок решит подняться на башню из коридора ему это удастся, а вот спуститься назад у него уже не получится. Игра ответит: «В этом направлении нельзя пойти.» Не самое приятное открытие для игрока. Хотя, бывают ситуации, когда проход должен работать только в одном направлении (порталы, телепорты, и т.п.), обычно, мы позволяем игроку вернуться назад в рамках одного уровня. Соединим новые локации с коридором:

var tower {
    // предыдущий код
    d: 'hall'
}

var round {
    // предыдущий код
    s: 'hall'

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

6.2 Свойство visits

У объекта room есть одно полезное свойство (вообще, у этого объекта не очень много свойств, так что хорошо, что хоть какое-то из них полезное). Это свойство visits.

Когда мы запускаем игру у всех комнат (кроме стартовой) это свойство отсутствует (undefined), но стоит игроку побывать в какой-то локации, как свойство visits этой локации принимает значение «1». При каждом следующем заходе оно будет увеличиваться на единицу.

Свойство visits удобно использовать, когда нужно вывести какое-то сообщение, когда игрок первый раз попадает в комнату, и в дальнейшем его не показывать.

Помимо описания локация может иметь название. Оно задается через свойство head.

var tower = {
    head: 'Башня'
}

Название локации выводится только когда игрок перемещается в нее.

Объект room можно использовать как для создания локации, так и для создания «экрана» для вывода произвольного текста, например, вступлений к главам и т.п. В последнем случае, свойство head не следует задавать. Однако, если вы создаете локацию очень желательно добавить ей свойство head. Дело в том, что движок ведет список (объект visitedLocs) посещенных локаций и осмотренных предметов, которые в ней находились на момент последнего посещения (можно посмотреть с помощью команды ЛОКАЦИИ). Если у локации отсутствует свойство head она не попадет в этот список.

Замечу, что свойство visits присутствует у всех объектов типа room, в которых побывал персонаж игрока, даже если у них нет свойства head.

Глава 7

Объект типа thing

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

var apple = {
    spec: 'thing',
    nam: ['яблоко', 'яблока', 'яблоку', 'яблоко', 'яблоком', 'яблоке', 'фрукт',
    'плод'],
    desc: 'Ярко-красный плод так и манит его съесть.',
    takeable: true, edible: true,
    loc: 'hall'
}

Как видите, у нас появилось много новых свойств. Давайте их рассмотрим.

7.1 Свойство gend

Свойство gend определяет род и число имени объекта. Это свойство может принимать следующие значения: m – мужской род, f – женский род, n – средний род, p – множественное число.

До сих пор мы не использовали в нашей игре свойство gend. Все дело в том, что движок автоматически определяет род (число) для объектов thing и player при старте игры. Таким образом, у всех объектов указанных типов есть свойство gend.

Протопарсер определяет род и число объекта по окончанию его основного имени (свойству nam[0]).

Таблица соответствия рода (числа) объекта и окончания его имени

Род (число) объекта Окончание основного имени объекта
Мужской все символы, не относящиеся к окончаниям женского, среднего рода и множественного числа
Женский а, ь, я
Средний о, е
Множественное число ы, и

Данное правило, однако, работает не всегда, поэтому при сознании объекта следует сверяться с таблицей. Если род (число) объекта не совпадают с указанным в таблице следует самостоятельно добавить в объект свойство gend с правильным значением. В главе 12.6 есть пример, когда мы задаем свойство gend объекту gate.

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

7.2 Свойство edible

Прочитав описание яблока, игрок наверняка решит его съесть. Тут нам и пригодится свойство edible. Теперь при вводе команды СЪЕСТЬ ЯБЛОКО игра выдаст «Вы съедаете яблоко.» При этом, яблоко исчезнет из инвентаря игрока. Иначе говоря, оно будет удалено из игры. Что же произойдет, если игрок вздумают попробовать на зуб, например, дверь? На все попытки съесть объект без свойства edible: true, игра неизменно будет отвечать «<Объект> нельзя употребить в пищу.»

7.3 Свойство takeable

По-умолчанию, все предметы жестко «закреплены» в своих локациях. Это сделано потому что большинство предметов в парсерных играх обычно составляют статичные «декорации». Если вам нужно, чтобы игрок мог взять предмет, добавьте ему свойство takeable: true. Не забудьте добавить takeable предметам, с которыми игрок начнет игру, иначе, выбросив их, он уже не сможет их вновь поднять.

7.4 Размещение предмета в нескольких локациях

Из предыдущих глав мы уже многое узнали о свойстве loc.

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

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

var stairs = {
    spec: 'thing',
    nam: ['лестница', 'лестницы', 'лестнице', 'лестницу', 'лестницой',
    'лестнице', 'ступеньку', 'ступень'],
    desc: 'Узкая винтовая лестница, на которой с трудом разминутся даже два человека, соединяет башню и коридор.',
    hidden: true,
    loc: ['hall', 'tower']
}

Теперь, игрок может получить доступ к лестнице как находясь в коридоре, так и стоя на башне.

Таким же образом добавим в коридор и круглую комнату стену.

var wall = {
    spec: 'thing',
    nam: ['стена','стены','стене','стену','стеной','стене', 'пыль', 'грязь',
    'камень', 'камни'],
    desc: 'Стена сложена из огромных серых камней, первоначальный цвет которых
    не представляется возможным определить из-за толстого слоя многовековой
    пыли и грязи, которыми они покрыты.',
    hidden: true,
    loc: ['hall', 'round']
}

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

7.5 Свойство text

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

var ticket = {
    nam: ['билет', 'билета', 'билету', 'билет', 'билетом', 'билете',
    'прямоугольник', 'текст'],
    spec: 'thing',
    desc: 'Картонный прямоугольник голубого цвета, на котором изящными
    золотистыми буквами напечатан какой-то текст.',
    text: '«Уважаемый посетитель # 3677! Мы рады приветствовать вас в Фантазии. 
    Пожалуйста, ни чему не удивляйтесь и чувствуйте себя как дома. Но, если
    вам и впрямь надо вернуться домой используйте шляпу».',
    loc: 'round',
    takeable: true
}

Вы, конечно, заметили новое свойство text. Само его наличие говорит о том, что предмет можно ПРОЧИТАТЬ. Значение же свойства, как не трудно догадаться, является тем текстом, который будет выведен в ответ на команду игрока ПРОЧИТАТЬ <ПРЕДМЕТ>.

7.6 Свойство worn

Отлично, кое-что уже прояснилось. Хотя, на самом деле, игрок получил скорее больше вопросов, чем ответов: что это за Фантазия: какая-то страна, парк развлечений? Как он попал сюда? Почему он не должен ничему удивляться, и что это за шляпа, которую он, очевидно, должен найти? Впрочем, на последний вопрос мы как раз сейчас ответим.

var hat = {
    spec: 'thing',
    nam: ['шляпа', 'шляпы', 'шляпе', 'шляпу', 'шляпой', 'шляпе'],
    desc: 'Серая широкополая шляпа выглядит весьма помятой.',
    loc: 'tower', // шляпа будет лежать в башне, где игрок не сразу ее найдет
    takeable: true
}

Если помните, мы пообещали игроку вернуть его домой, если он использует шляпу. Мы только забыли сказать, как он должен ее использовать. Думаю, вы со мной согласитесь, что наиболее очевидный способ «использовать» шляпу – НАДЕТЬ ее. Сделаем нашу шляпу «надеваемой»:

worn: false

Этой короткой строкой мы сообщили интерпретатору сразу две вещи о нашем предмете: во-первых, что его можно надевать (присутствует свойство worn), во-вторых, что предмет не надет (false). Если вы делаете предмет «надетым» (worn: true) не забудьте сразу поместить его в инвентарь (loc: 'player').

Секреты шляпы на этом не заканчиваются. Мы еще к ней вернемся в главе «Объект events», а пока рассмотрим еще несколько свойств.

7.7 Свойства door, closed, locked

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

var heavy_door = {
    spec: 'thing',
    nam: ['дверь', 'двери', 'двери', 'дверь', 'дверью', 'двери'],
    desc: 'Тяжелая дубовая дверь, кажется, помнит еще те времена, когда слово
    «рыцарь» было не просто красивым эпитетом.',
    hidden: true,
    loc: ['round','hall'],
    closed: true,
    locked: true,
    door: true
}

В этом примере у нас появились сразу три новых свойства. Начнем со свойства door.

Именно свойство door делает предмет потенциальным препятствием на пути игрока из одной комнаты в другую. Кстати, само понятие «дверь» довольно условно. Ваш предмет может быть порталом, межзвездными вратами или чем-то еще. Однако, одного свойства door еще недостаточно, чтобы он мог в полном смысле слова называться «дверью». Необходимо расположить его в локациях, которые он связывает (loc: ['round','hall']). Важно только помнить, что конкретная дверь не должна иметь имя door.

Если свойство door можно сравнить с дверным проемом и петляли, то следующее свойство – closed, можно уподобить двери. Оно может иметь одно из двух состояний: открыто (closed: false), либо закрыто (closed: true). Если объект со свойством door закрыт, игрок не сможет пройти через него, пока он не будет открыт.

Каждое из этих свойств, которые мы рассмотрели, по отдельности, всего лишь элементы, и только вместе они делают предмет дверью.

Последнее свойство, которое нам осталось рассмотреть – locked, продолжая начатую выше аналогию, можно сравнить с замком. Как и любой замок, это свойство может иметь одно из двух состояний: заперто (locked: true) или не заперто (locked: false). Запертую дверь, в отличии от незапертой нельзя ОТКРЫТЬ, но можно ОТПЕРЕТЬ. Думаю, вы и так это знали.

Вообще говоря, свойства closed и locked прекрасно работают в паре даже без свойства door. Да, дверь не единственная вещь, которую можно открывать и закрывать, отпирать и запирать. Например, в вашей игре может быть водопроводный кран. Добавьте ему свойство closed и игрок сможет его ОТКРЫТЬ и ЗАКРЫТЬ.

7.8 Свойство moved

Обычно, предметы в игре лежат там, куда положил их автор и спокойно дожидаются, когда игрок удостоит их своим вниманием. Протопарсер может отслеживать менял ли предмет свое начальное местоположение. Для этой цели служит свойство moved. У «нетронутых» предметов оно отсутствует. Если предмет был перемещен (не обязательно игроком) он приобретает свойство moved, которое становится равным 1. Кстати, свойство moved также добавляется объекту player как только персонаж игрока переместился в другую локацию. Благодаря этому можно отслеживать сколько передвижений совершил персонаж.

7.9 Свойство examined

В прошлой главе мы рассмотрели свойство visits объекта room. У предметов есть похожее свойство examined. Каждый раз, когда пользователь осматривает какой-то предмет, значение свойства examined этого предмета увеличивается на единицу. Таким образом, можно определить сколько раз игрок осматривал предмет. Если предмет не был осмотрен ни разу значение свойства будет равно undefined. Свойство examined удобно использовать, когда нужно вывести определенный текст при первом осмотре предмета.

7.10 Свойство hiddenPossession

В инвентаре бывает удобно «хранить» объекты, которые всегда доступны персонажу игрока. Это могут быть как системные объекты, так и игровые, например, аура, внутренний голос, киберимплант, жучок... В таких случаях может возникнуть необходимость скрыть предмет, находящийся у игрока, но не являющийся «инвентарем» в прямом смысле этого слова. Для этого существует свойство hiddenPossession.

Если значение свойства равно true объект не будет выводится в списке предметов, находящихся в инвентаре. В остальном же он будет вести себя как обычный предмет в инвентаре. Если вы решите проверить размер инвентаря с помощью функции getObjByKV('loc','player').length, то обнаружите, что скрытый объект занимает в инвентаре место наравне с обычными видимыми объектами. Стоит это учитывать, если в свой игре вы решите ограничить размер инвентаря с помощью свойства player.maxCarried. Стоит учитывать и тот факт, что игрок может попробовать взаимодействовать со скрытым объектом.

> Бросить ауру
Вы оставляете здесь ауру.

Чтобы объекты правильно реагировали на определенные действия пользователя необходимо добавить им методы. О методах будет рассказано в главе 9.

7.11 Свойство sceneDesc

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

Во-первых, можно дать упоминание о предмете прямо в описании локации (следует также добавить ему свойство hidden). Это неплохой способ для статичных объектов, которые не меняют своего положения, но он не подходит для «мобильных» объектов.

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

Наконец, третий способ – использовать свойство sceneDesc. В качестве значения мы даем описание объекта в контексте описании локации. Проще говоря, то, какую информацию об объекте игрок получит из описания локации. При использовании данного свойства имя объекта не попадает в список «Здесь есть», появляющийся после описания локации.

У нас уже есть яблоко, так что теперь в качестве примера создадим косточку.

var seed = {
    spec: 'thing',
    nam: ['косточка', 'косточки', 'косточке', 'косточку', 'косточкой',
    'косточке', 'кость', 'семя', 'семечко', 'зернышко',
    'зерно'],
    desc: 'Крохотное золотистое зернышко чуть подрагивает словно живое.
    Кажется, ей не очень здесь нравится. Наверное, она хочет туда, где ей
    будет хорошо.',
    sceneDesc: 'Золотистое зернышко крутится и подпрыгивает на каменном полу.',
    loc: '',
    takeable: true
}

Обратите внимание, свойству loc мы задали пустое значение. Таким образом, на начало игры косточка будет недоступна. Сейчас это не важно, позднее мы еще вернемся к этому примеру.

Теперь, если косточка окажется на полу, осмотревшись, пользователь получит сообщение: «Золотистое зернышко крутится и подпрыгивает на каменном полу». Гораздо лучше, чем простое «Здесь есть: косточка».

Важно помнить, что если у объекта есть свойство hidden: true, то ни имя объекта, ни значение его свойства sceneDesc не будут присутствовать в описании локации.

Глава 8

Обработка команд

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

Интерфейс командной строки протопарсера прост, он оперирует всего двумя понятиями: глагол и существительное. Команда пользователя может состоять либо из одного глагола, либо из глагола и существительного. Ни больше, ни меньше.

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

ОК, представим невозможное: программа поняла глагол и существительное (если оно было), что происходит дальше?

В игру вступает функция choiceHandler. Прежде всего, она проверяет существительное рядом с глаголом: введено ли оно, есть ли в текущей локации объект с таким именем, требуется ли для данного глагола существительное вообще?

Описанные выше процедуры выполняются всегда, автор не может их отменить или изменить. А вот дальше наступает самое интересное. Интерпретатор последовательно выполняет серию проверок, пытаясь найти в определенных объектах метод, который обработает команду (перехватит ее у функции-обработчика). Имя искомого,«метода-перехатчика» должно совпадать с именем функции-обработчика команды (см. «Стандартные функции-обработчики (API)»).

Методы создает автор игры. По-сути, это небольшие кусочки кода, которые обрабатывают определенную команду пользователя при наступлении определенных условий. Подробнее о методах будет рассказано в следующей главе. Сейчас же следует знать, что метод, обычно, возвращает логическое значение (true/false).

Метод возвращает true, если команда пользователя обработана, и false – если нет.

Что означает «возвращает...» и «команда пользователя обработана»? Помимо методов, которые пишет автор существуют стандартные функции-обработчики. Если автор не написал метода для какой-то команды, то ее обработкой займется стандартная функция. Однако, иногда, после того как интерпретатор выполнил какой-то метод, необходимо, чтобы стандартная функция завершила обработку. Иначе говоря, метод должен сообщить движку, что обработка команды не завершена (return false). Если же метод все сделал сам, и стандартной функции-обработчику беспокоиться ни о чем не надо, он возвращает true (return true).

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

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

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

  1. В начале, если введенная команда не является системной (метакоманда), интерпретатор увеличивает значение свойства game.turn на единицу.
  2. Введенная команда добавляется в начало массива game.commandHistory (исключения: команды ПОВТОРИТЬ, ИСТОРИЯ, а также, команды уже находящиеся в истории команд в массив game.commandHistory не попадают). Если число команд в массиве при этом превысит значение game.commandHistoryLength последний элемент массива удаляется.
  3. Если у текущей локации есть свойство head, ее идентификатор добавляется в качестве имени свойства объекта visitedLocs. Свойству присваивается массив имен объектов (свойство nam[3]), находящихся в этой локации, у которых свойство examined не равно undefined.
  4. Интерпретатор проверяет есть ли метод beforeAll у объекта events, и если есть, то выполнит его. Если метод вернет true, интерпретатор перейдет к п.9.
  5. Метод ищется в объекте относительно которого вызвана команда (если он есть). Если метод вернул true, интерпретатор перейдет к п.9, если false – к п.8.
  6. Если метод не найден, проверка идет среди свойств текущей локации. Если метод вернул true, интерпретатор перейдет к п.9, если false – к п.8.
  7. Если метод до сих пор не найден проверяется есть ли он у объекта globalVerbs. Если метод вернул true, интерпретатор перейдет к п.9, если false – к п.8.
  8. Если метода нет ни в globalVerbs, ни в текущей локации, ни в объекте действия, то, если в объекте game есть свойство с именем вызываемой функции выводит значение этого свойства, иначе вызывается стандартная функция-обработчик.
  9. После того, как команда обработана интерпретатор проверяет есть ли у объекта events метод afterAll, и, если есть, то вызовет его.

После ввода любой команды значение game.commandHistoryIndex становится равным -1.

Если в вашей игре отсутствуют объекты events или globalVerbs движок на этапе загрузки игры создаст соответствующие пустые объекты events и globalVerbs. Это необходимо для корректной работы приложения.

Что произойдет, если метод присутствует у нескольких объектов? Будет выполнен тот, который первым расположен в цепочке.

Подробнее об объектах events и globalVerbs будет рассказано в соответствующих главах.

Глава 9

Методы объектов

Стандартные обработчики команд – это хорошо. Но, иногда вам может потребоваться написать для какого-нибудь действия нестандартную реакцию. К счастью, протопарсер позволяет это сделать. По-умолчанию, любой предмет можно выбросить из инвентаря. Давайте сделаем наш билет «невыбрасываемым». Для этого добавим в объект ticket свойство drop

var ticket = {
    nam: ['билет', 'билета', 'билету', 'билет', 'билетом', 'билете',
    'прямоугольник', 'текст'],
    spec: 'thing',
    desc: 'Картонный прямоугольник голубого цвета, на котором изящными
    золотистыми буквами напечатан какой-то текст.',
    text: '«Уважаемый посетитель # 3677! Мы рады приветствовать вас в Фантазии.
    Пожалуйста, ни чему не удивляйтесь и чувствуйте себя как дома. Но, если
    вам и впрямь надо вернуться домой используйте шляпу».',
    loc: 'round',
    takeable: true,
    drop: function() {
        p('Билет еще пригодится.')
    return true
    }
}

После имени метода (который должен совпадать с именем заменяемой стандартной функции-обработчика) через двоеточия мы пишем функцию которая выводит (p()) строку "Билет еще пригодится.".

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

Поздравляю! Мы создали метод drop объекта ticket. Выполнив последнюю инструкцию (или дойдя до инструкции return) метод передает управление обратно парсеру. Теперь, если вы введете команду БРОСИТЬ БИЛЕТ интерпретатор выдаст строку:

Билет еще пригодится.

Билет, при этом, останется у нас. Что же произошло? В данном случае, пользовательская функция заменила стандартную функцию drop.

А что бы произошло, если бы мы написали return false? В этом случае, интерпретатор посчитал бы, что команда не обработана, и выполнил бы после нашего метода стандартную функцию drop.

>БРОСИТЬ БИЛЕТ
Билет еще пригодится.
Вы оставляете здесь билет.

В последнем случае мы могли бы вовсе не писать return false. По-умолчанию, если метод не возвращает true считается, что команда не обработана.

Рассмотрим пример посложнее. Если помните, ранее мы уже создали яблоко и косточку. Давайте сделаем так, чтобы после того, как мы съедим яблоко у нас останется косточка.

Создадим метод eat у нашего яблока.

eat: function() {
    remove(this);
    move(seed, 'player');
    p('Яблоко было таким вкусным, что вы даже не заметили, как оно исчезло
    у вас во рту, а вместо него в руках оказалась косточка.')
    return true
}

В данном примере наша функция не принимает аргументов. Поскольку объектом действия метода является сам объект, аргумент излишен. Первый оператор в теле функции - remove предназначен для удаления объекта (об особенностях использования этой функции можно прочитать в Приложении 1 «Стандартные функции-обработчики (API)»). Его аргумент - this ссылается на объект относительно которого вызван метод - apple. Вместо remove(this) мы могли бы написать и так: remove(apple).

Следующая инструкция move(seed, 'player') присваивает свойству loc косточки значение 'player', т.е. переносит ее в инвентарь. Вообще, функция move используется в стандартных функциях-обработчиках таких как take, drop, walk. Одной из ее задач является проверка лимита предметов в инвентаре. Кроме того, перемещая предмет впервые, функция move добавляет ему свойство moved, делая его равным 1, а в дальнейшем при каждом вызове увеличивает значение свойства на 1, тем самым указывая на количество раз, которое предмет менял свое местоположение.

Вместо функции move мы могли бы просто присвоить объекту новую локацию:

seed.loc = 'player'

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

С инструкциями p и return вы уже знакомы.

До сих пор мы присваивали методы исключительно предметам. В следующем примере мы присвоим метод комнате, и, заодно, познакомимся с аргументами. Не будем ходить далеко, и создадим у объекта hall метод say.

say: function(obj, word) {
    say(undefined, word);
    p('Эхо коридора чужим голосом повторило «' + capitalize(word) + ', ' + word + ', ' + word + '...».')
    return true
}

Если вы еще не догадались, только что мы создали эхо в нашем коридоре. Когда игрок произносит что-то, таинственное эхо повторяет сказанное за ним.

> СКАЗАТЬ ПРИВЕТ
Вы говорите «Привет».
Эхо коридора чужим голосом повторило «Привет, привет, привет...»

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

Наша функция имеет 2 параметра: obj и word. Иначе говоря, она готова принять от внутренней функции-обработчика 2 аргумента. На самом деле, функция-обработчик choiceHandler передает вызываемой функции 4 аргумента: объект для действия, необязательный аргумент, глагол и существительное. Разберем каждый из этих аргументов.

Объект для действия – то над чем совершается действие (ВЗЯТЬ КЛЮЧ, ОТКРЫТЬ ДВЕРЬ, ОСМОТРЕТЬ ПОЛ). Если данный объект находится в текущей локации или в инвентаре, функция-обработчик передает этот объект первым аргументом. Иногда в команде пользователя нет объекта. Например, СКАЗАТЬ ПРИВЕТ. Очевидно, что слово «привет» не является объектом, и функция-обработчик, в этом случае, передаст undefined, однако мы, все-равно, должны его обработать. Для этого есть необязательный аргумент.

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

Глагол, который употребил игрок: ПРЫГНУТЬ или ПОДПРЫГНУТЬ, НЮХАТЬ или ПОНЮХАТЬ.

Существительное, которое употребил игрок: СТАРИК или ДЕД, ЯБЛОКО или ФРУКТ.

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

Вернемся к нашему примеру.

У нашей функции, как вы помните, 2 параметра obj и word. Фактически, это переменные. Как и любые переменные они могут иметь произвольные имена. Первый параметр – объект, второй – необязательный аргумент. Значение первого аргумента – undefined, второго – слово, которое было введено после команды СКАЗАТЬ. По-сути, нам не нужен первый аргумент, но, если вы помните, последовательность аргументов важна, поэтому мы указываем, что наша функция принимает 2 аргумента.

Первой инструкцией в теле функции – say(undefined, word) мы вызываем стандартную функцию-обработчик say, которая выводит строку «Вы говорите .» Обратите внимание, стандартные функции-обработчики принимают определенное число аргументов строго определенного типа (см. Приложение 1 «Стандартные функции-обработчики (API)»). Функция say принимает 2 аргумента, первый из которых может иметь любое значение (обычно, undefined), а второй – строка.

Последняя команда вполне очевидна. Функция capitalize возвращает переданную ей строку word в которой первая буква переведена в верхний регистр.

Хорошо, но что, если игрок «не догадается» что-то СКАЗАТЬ? Неужели мы напрасно старались, создавая эхо? Мы можем намекнуть игроку что-нибудь СКАЗАТЬ (например, ПРИВЕТ), поместив в зал рыцарский доспех. Дополним вначале описание коридора:

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

А теперь, добавим и сам доспех:

var armour = {
    spec: 'thing',
    nam: ['доспех', 'доспеха', 'доспеху', 'доспех', 'доспехом','доспехе',
    'доспехи', 'рыцаря', 'забрало', 'шлем'],
    desc: 'Потемневший от времени доспех даже сейчас производит угрожающее
    впечатление. Доспех несомненно пуст, и все-же вам не хотелось бы
    оставаться с ним наедине.',
    hidden: true,
    loc: 'hall',
    take: function() {
        p('Доспех слишком тяжел для вас.')
        return true
    },
    wear: function() {
        p('Глупо таскать такую кучу металла на себе.')
        return true
    }
}

Продолжим создавать методы. В главе «Объект типа room» мы познакомились со свойством комнат visits. Это свойство «знает» посещали ли мы какую-то локацию, и даже может сказать сколько раз мы в ней были.

Наша цель – создать метод, который выполнится всего один раз, когда мы впервые поднимемся на башню.

Добавим локации hall метод walk:

walk: function(obj, dir) {
    if (dir == 'u' && !tower.visits) p('После полумрака коридора яркий
    дневной свет на несколько секунд ослепляет вас. Открывшаяся вашему взору
    картина завораживает.')
}

Вы можете спросить, почему мы назначили метод walk локации hall, а не tower? Вообще, в большинстве языков программирования, практически любую задачу можно решить несколькими способами. В нашем случае мы могли бы поместить наш метод (с небольшими изменениями) в объект globalVerbs или использовать метод beforeAll объекта events.

Метод walk срабатывает в момент, когда введена команда на перемещение. Единственный способ попасть в башню - из коридора. Поэтому мы «вешаем» наш перехватчик в локацию hall. Однако, это не все. Мы должны убедиться, что пользователь ввел команду ВВЕРХ, а не СЕВЕР или, скажем, ВНИЗ. Для этого проверяем значение аргумента dir: dir == 'u'. Кроме того, мы должны убедиться, что персонаж игрока еще не был в башне: !tower.visits, т.е. у башни должно отсутствовать свойство visits.

Если обратили внимание, в этом раз мы не использовали инструкцию return true. Наша команда еще не до конца обработана. Мы забыли переместить игрока в башню. Мы могли бы это сделать сами, добавив строку walk(undefined, 'u'). Правда, в этом случае нам пришлось бы добавить еще и инструкцию return true, поскольку иначе, после того как персонаж окажется в башне, движок, не зная о том, что мы уже переместили персонажа, попытается вновь выполнить команду ВВЕРХ, и не найдя в локации tower выхода вверх выдаст сообщение «В этом направлении нельзя пойти.»

Как видите, проще позволить протопарсеру выполнить перемещение персонажа за нас. К счастью, движок помнит исходную команду и знает, что нужно с ней делать.

Наш метод может обрабатывать не только команду на движение вверх, а вообще на любое движение. Если условие dir == 'u' будет ложно (игрок ввел ВНИЗ или СЕВЕР) управление будет передано стандартной функции-обработчику walk.

Глава 10

Объект globalVerbs

Объект globalVerbs уже вкратце упоминался в предыдущих главах. Настала пора познакомиться с ним поближе. В отличии от всех предыдущих объектов, которые мы уже успели рассмотреть, свойствами globalVerbs являются сплошь методы. Данные методы переопределяют стандартные функции-обработчики на «глобальном уровне». К примеру, вы можете «запретить» в вашей игре инвентарь.

var globalVerbs = {
    inventory: function() {
        p('В этой игре нет инвентаря.')
        return true
    }
}

Теперь, в какой бы локации не находится персонаж игрока на попытки ввода команды ИНВЕНТАРЬ (или И) игра ответит: «В этой игре нет инвентаря.» Однако, если пользователь решит ВЗЯТЬ какой-то предмет, игра без возражений выполнит эту команду. Разумеется, как и в случае с инвентарем, мы можем с помощью globalVerbs перехватить команду ВЗЯТЬ:

var globalVerbs = {
    inventory: function() {
        p('В этой игре нет инвентаря.')
        return true
    },
    take: function() {
        p('Ха-ха-ха! В этой игре вы ничего сможете взять!')
        return true
    }
}

Стоит отметить, что если в вашей игре есть объекты (предметы или локации) у которых есть метод take, то при определенных условиях (см. главу «Обработка команд») выполнится метод take этих объектов, а не globalVerbs. А что, если вам нужно полностью перехватить какую-то команду, не задумываясь, есть ли где-то в коде объекты с методом take? Такой способ есть. Нужно использовать метод beforeAll объекта events. Как это сделать будет рассказано в следующей главе.

В нашей игре мы переопределим стандартную функцию sleep, «отзывающуюся» на команды СПАТЬ, ПОСПАТЬ, ЗАСНУТЬ.

var globalVerbs = {
    sleep: function() {
        p('Едва ли вам теперь удастся заснуть.')
        return true
    }
}

На самом деле, поскольку в нашем примере единственное, что делает функция sleep – выводит текст мы могли поступить еще проще определив у объекта game свойство sleep:

var game = {
    ...
    sleep: 'Едва ли вам теперь удастся заснуть.'
}

Результат будет аналогичный.

В целом, методы globalVerbs работают по тем же правилам, что и методы других объектов.

Глава 11

Объект events

Объект events, как и globalVerbs состоит только из методов. Однако, их у этого объекта всего три: init, beforeAll и afterAll. Первый из них срабатывает всего один раз за игру – сразу после загрузки. Данный метод не принимает никаких аргументов и не требует инструкции return. Метод beforeAll срабатывает до того, как команда пользователя будет передана функции-обработчику, а afterAll – после3. Эти методы, если они присутствуют в вашей игре, будут вызываться всегда, независимо от того, что ввел пользователь4. Поэтому, их особенно удобно использовать для отслеживания наступления определенных событий.

Простой пример. Добавим небольшой вводный текст в начало игры:

var events = {
    init: function() {
        p('Вы стоите в длинном широком коридоре какого-то старого особняка или даже замка, не имея ни малейшего понятия как здесь очутились, и, что еще важнее, как отсюда выбраться.')
    }
}

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

В прошлой главе я упомянул, что с помощью метода beforeAll можно перехватить команду пользователя, даже если существуют методы для ее обработки. Как же это сделать? Продолжим пример с командой ВЗЯТЬ. Раннее, для перехвата этой команды мы использовали метод take. Вот как это можно сделать, используя метод beforeAll:

var events = {
    beforeAll: function(com) {
        if (com == 'take') {
            p('Ха-ха-ха! В этой игре вы ничего сможете взять!')
            return true
        }
    }
}

Теперь, даже если в вашей игре есть объекты с методом take они не сработают, потому что метод beforeAll выполнится раньше них.

Методы beforeAll и afterAll немного отличаются от прочих методов. Вы, наверное, обратили внимание на аргумент com у нашей функции. В отличии от других методов, функция choiceHandler, вызывая методы объекта events, передает им пять аргументов: имя стандартной функции-обратчика команды, объект для действия, необязательный аргумент, глагол, существительное.

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

Вернемся к старому примеру с шляпой из раздела «7.6 Свойство worn». Ее «волшебные» свойства не ограничиваются worn.

var hat = {
    spec: 'thing',
    nam: ['шляпа', 'шляпы', 'шляпе', 'шляпу', 'шляпой', 'шляпе', 'надпись',
    'ленту'],
    desc: 'Серая широкополая шляпа выглядит весьма помятой. Сбоку к шляпе
    приколота какая-то лента с надписью.',
    loc: 'tower',
    takeable: true, worn: false
}

Я немного изменил описание шляпы. Если игрок введет ОСМОТРЕТЬ ШЛЯПУ, игра сообщит, что на ленте, которая приколота к шляпе есть какая-то надпись, которую игрок, разумеется, захочет прочитать. Однако, как вы заметили, у шляпы отсутствует свойство text. Чтобы прочитать текст на шляпе игроку вначале придется ее взять.

read: function() {
    if (hat.loc == 'player') p('Надпись на ленте гласила: «Надень меня,
    скажи «Домой!», и дом увидишь свой родной».')
    else p('Вам придется взять в руки шляпу, чтобы прочитать текст на
    ленте.')
    return true
}

Ничего не забыли? А что если игрок захочет ПРОЧИТАТЬ надетую шляпу?

read: function() {
    if (hat.loc == 'player' && !hat.worn) p('Надпись на ленте гласила:
    «Надень меня, скажи «Домой!», и дом увидишь свой родной».')
    else p('Вам придется взять в руки шляпу, чтобы прочитать текст на
    ленте.')
    return true
}

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

У игрока есть целых два способа завладеть шляпой. Он может ВЗЯТЬ ШЛЯПУ либо НАДЕТЬ ШЛЯПУ. Можно перехватить обе эти команды, добавив шляпе методы take и wear, но можно поступить проще:

var events = {
    init: function() {
        p('Вы стоите в длинном широком коридоре какого-то старого особняка или даже замка, не имея ни малейшего понятия как здесь очутились, и, что еще важнее, как отсюда выбраться.')
    },
    beforeAll: function(com, obj) {
        // Шляпа
        if ((com == 'take' || com == 'wear') && obj == hat && !hat.moved) {
            p('Едва вы успели коснутся шляпы, внезапный порыв ветра
            подхватил ее, и понес в сторону озера.')
            move(hat, 'lake')
            return true
        }
    }
}

Я специально включил в этот пример ранее созданное нами вступление, чтобы у вас было цельное представление о работе методов объекта events. Как мы уже узнали ранее методы beforeAll и afterAll могут использовать аргументы, которые им передает вызывающая функция. В последнем примере мы использовали два аргумента, чтобы получить имя функции-обработчика и объект действия.

Метод afterAll срабатывает самым последним, перед тем, как управление перейдет к парсеру, поэтому в нем не используются инструкции return true / return false.

Глава 12

Продолжение «Фантазии»

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

12.1 Ключи

В разделе «7.7 Свойства door, closed, locked» мы создали дверь, ведущую в круглую комнату, в которой находится билет.

var heavy_door = {
    spec: 'thing',
    nam: ['дверь', 'двери', 'двери', 'дверь', 'дверью', 'двери'],
    desc: 'Тяжелая дубовая дверь, кажется, помнит еще те времена, когда слово
    «рыцарь» было не просто красивым эпитетом.',
    hidden: true,
    loc: ['round','hall'],
    closed: true,
    locked: true,
    door: true
}

Наша дверь очень простая: открывается по команде ОТКРЫТЬ и отпирается по команде ОТПЕРЕТЬ. Можно было бы немного усложнить нашу игру, сделав, чтобы дверь отпиралась (и запиралась) только ключом. Теперь, когда мы познакомились с методами мы можем это сделать.

unlock: function() {
    if (heavy_door.locked && heavy_door.closed) {
        if (key.loc == 'player') p('Вы вставляете ключ в замочную
        скважину и с большим трудом проворачиваете его.')
        else {
            p('Без ключа дверь не открыть.')
            return true
        }
    }
}

Добавим двери и метод lock:

lock: function() {
    if (!heavy_door.locked && heavy_door.closed) {
        if (key.loc == 'player') p('Вы вставляете ключ в замочную
        скважину и с большим трудом проворачиваете его, придавливая рукой
        тяжелую дверь.')
        else {
            p('Без ключа дверь не запереть.')
            return true
        }
    }
}

А что же ключ? Вот и он:

var key = {
    spec: 'thing',
    nam: ['ключ', 'ключа', 'ключу', 'ключ', 'ключом', 'ключе'],
    desc: 'Тяжелый бронзовый ключ.',
    loc: 'player',
    takeable: true
}

12.2 Ограничение передвижения

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

var tower = {
    spec: 'room',
    head: 'Башня',
    desc: 'Открытая площадка башни позволяет видеть округу на много километров.
    В отдалении на севере блестит небольшое озеро и виднеется выход из парка.
    Вниз ведет лестница.',
    d: 'hall',
    walk: function(obj, dir) {
        if (dir != 'u' && dir != 'd') { p('Слишком высоко. Надо найти
        какой-то другой способ спуститься вниз.')
        return true
        }
    }
}

Он может попытаться спрыгнуть.

jump: function() {
    p('С такой высоты? Ну, уж нет!')
    return true
}

Однако, у него ничего из этого не выйдет, поскольку мы перехватили все соответствующие стандартные функции.

12.3 Изменение локаций

Хорошо, но как же игроку все-таки спуститься? Не забыли про яблоко, которое мы любезно оставили игроку? Если помните, когда мы съедали плод, у нас в руках оказывалась косточка (волшебная, как вы, конечно, догадались). Сделаем так, что, если игрок бросит косточку с башни, из земли вырастет дерево, по которому можно будет спуститься вниз.

Добавим косточке метод drop:

var seed = {
    spec: 'thing',
    nam: ['косточка', 'косточки', 'косточке', 'косточку', 'косточкой',
    'косточке', 'кость', 'семя', 'семечко', 'зернышко', 'зерно'],
    desc: 'Крохотное золотистое зернышко чуть подрагивает словно живое. 
    Кажется, ей не очень здесь нравится. Наверное, она хочет туда, где
    ей будет хорошо.',
    sceneDesc: 'Золотистое зернышко крутится и подпрыгивает на каменном полу.',
    drop: function() {
        if (player.loc == 'tower') {
            p('Едва косточка коснулась земли, как стены башни и пол
            затряслись, послышался громкий треск сучьев, словно какой-то
            великан прокладывал себе дорогу через чащу леса. В
            действительности же, дорогу себе прокладывало огромное дерево,
            непонятно как за какие-нибудь пять минут достигшее своей
            верхушкой вершины башни.')
            remove(this)
            tower.desc = 'Открытая площадка башни позволяет видеть округу на
            много километров. Озеро на севере заграждают кроны огромного
            дерева, вплотную примкнувшие к башне. Вниз ведет лестница.'
            tower.n = 'tree'
            objTree.loc = ['tower', ' tree', 'glade']
            return true
        }
    },
    loc: '',
    takeable: true
}

В этом примере мы впервые изменили описание локации, и добавили новый выход. Нам осталось добавить в нашу игру локацию и объект «дерево». Поскольку все объекты должны иметь разные разные имена назовем новую локацию tree, а объект дерево - objTree.

var tree = {
    spec: 'room',
    head: 'Вершина дерева',
    desc: 'Широкие кроны дерева позволяют вам свободно, и, даже, с некоторым
    удобством разместиться на верхушке. Прямо под вами далеко внизу раскинулась
    залитая солнцем поляна, а в нескольких метрах с южной стороны высится еще
    одна громада - старая башня.',
    s: 'tower',
    d: 'glade'
}

var objTree = {
    spec: 'thing',
    nam: ['дерево', 'дерева', 'дереву', 'дерево', 'деревом', ' дереве',
    'крону', 'лист', 'листья'],
    desc: 'Эта громада производит впечатление спящего великана, тяжело
    покачиваясь и шумя листьями при сильных порывах ветра. Ее широкие
    кроны опираются на башню, доставая до самой ее вершины.',
    hidden: true,
    loc: ''
}

Отлично, но, если мы сейчас попробуем перейти с башни на дерево (команда СЕВЕР) у нас ничего не получится. Помните, мы добавили в локацию tower метод walk, который на все попытки двигаться в любом направлении кроме ВВЕРХ И ВНИЗ выдавал сообщение «Слишком высоко...»? Что же делать, неужели придется переписывать условие? К счастью, есть способ лучше.

var events = {
    beforeAll: function(com, obj, optional) {
        if (player.loc == 'tower' && optional == 'n' && tower.n == 'tree') {
            p('Вы осторожно перебираетесь на верхушку дерева.')
            walk(undefined, 'n')
            return true
        }
    }
}

Как видите, мы добавили в метод beforeAll еще один аргумент – optional, который нужен нам, чтобы получить направление движения персонажа игрока. Заметьте, метод walk башни продолжает, как и раньше, перехватывать команды на движение (за исключением движения на север).

Прежде чем мы двинемся дальше осталось решить еще одну проблему. Что если игрок не возьмет шляпу, а сразу воспользуется косточкой? Это нарушит последовательный ход развития истории, чего бы нам не хотелось. А что если, игрок обнаружит яблоко под шляпой, в тот момент, когда ветер унесет ее прочь?

Установим свойство loc яблока равным '', и изменим beforeAll:

if ((com == 'take' || com == 'wear') && obj == hat && !hat.moved) {
    p('Едва вы успели коснутся шляпы, внезапный порыв ветра подхватил ее,
    и понес в сторону озера. С удивлением вы обнаруживаете яблоко на том месте,
    где только что лежала шляпа.')
    move(hat, 'lake')
    apple.loc = 'tower'
    return true
}

Возможно, требует пояснения строка apple.loc = 'tower', ведь мы могли также написать move(apple, 'tower'). На самом деле, в нашей игре это не принципиально, однако, предположим, что в какой-то момент, до того, как игрок возьмет яблоко, вы захотите проверить свойство moved яблока. Если бы мы использовали функцию move, свойство moved яблока было бы равно 1, хотя, с начала игры яблоко никуда не перемещалось, а, как мы знаем, находилось под шляпой.

Вернемся снова к рыцарскому доспеху. Игрок, наверняка, захочет заглянуть внутрь. Было бы неплохо дать ему эту возможность. Для начала, добавим доспеху свойство closed: true. Можно еще немного усложнить нашу игру, найдя дополнительное применение для него. Мы предположили, что игрок захочет заглянуть внутрь доспеха. Было бы справедливо вознаградить его любопытство, положив туда ключ, поэтому изменим свойство loc ключа:

loc: 'armour'

Вообще говоря, предмет может «принадлежать» либо локации, либо персонажу игрока (см. раздел «5.5 Инвентарь»). Фактически, с точки зрения игры, ключа в доспехе нет. Игрок не может ВЗЯТЬ КЛЮЧ из доспеха, ни как-либо иначе с ним взаимодействовать. Поэтому, когда игрок откроет шлем нам придется каким-то образом изъять оттуда ключ.

Добавим методы open и close в armour:

open: function() {
    if (key.loc == 'armour') {
        p('Едва вы подняли  забрало, что-то блестящее выскочило из шлема и со
        звоном упало на каменный пол.')
        move(key, 'hall')	
    } else if (armour.closed) 
        p('Вы поднимите забрало и заглядываете внутрь, но там только паутина.')
    else p('Забрало уже поднято.')
    armour.closed = false
    hall.desc = hall.desc.replace('опущенным', 'поднятым')
    return true
},
close: function() {
    if (armour.closed) p('Забрало уже опущено.')
    else p('Вы опускаете забрало.')
    hall.desc = hall.desc.replace('поднятым', 'опущенным')
    armour.closed = true
    return true
} 

Оба метода полностью заменяют стандартные функции-обработчики (возвращают true), поскольку стандартные реакции «Вы открываете (закрываете) доспех.» нас не устраивают.

Вы, наверняка, заметили новый метод replace, который мы применили к описанию локации hall.desc. В этой главе мы уже меняли описание локации простым присваиванием свойству нового значения взамен старого. Однако, есть другой, более экономный способ сделать это. Метод replace является стандартным методом объектов типа String. Он заменяет текст в строке, используя регулярное выражение или строку, которую мы передаем первым аргументом, на строку из второго аргумента. Метод replace удобно использовать, когда вам нужно поменять не весь текст, а лишь отдельные слова в нем.

На случай, если игрок не догадается открыть шлем добавим небольшую подсказку (в объект armour).

attack: function() {
    if (key.loc == 'armour') p('Внутри доспеха что-то звякнуло.')
    else p('Доспех отозвался глухим звуком удара.')
    return true
}

12.4 Персонажи

Ну, хорошо, мы уже много где побывали, научились работать с локациями и предметами, и, даже посадили дерево. А как же другие персонажи? Неужели наш турист обречен на одинокое скитание? Разумеется, нет! Компанию нашему герою составит старик-лодочник. Создадим вначале поляну, на которой он будет «обитать»:

var glade = {
    spec: 'room',
    head: 'Поляна',
    desc: 'Небольшая поляна залита солнечным светом. В центре поляны высится
    огромное дерево. На севере поблескивает на солнце небольшое озеро,
    прохладный ветерок с которого вас приятно освежает. На берегу, рядом с
    водоемом, ссутулившись, сидит, глядя на воду старик. В метре от него к
    берегу причалена старая лодка. Вдалеке, на северо-востоке виднеется выход
    из парка.',
    u: 'tree',
    n: 'lake',
    ne: 'entrance'
}

Теперь, можно приняться и за старика:

var oldman = {
    spec: 'thing',
    nam: ['старик', ' старика', 'старику', 'старика', 'стариком', 'старике',
    'лодочника', 'шорты', 'телогрейку', 'старичка', 'деда'],
    desc: 'Невысокий лысенький старичок в поношенной телогрейке и в цветастых 
    молодежных шортах, неизвестно как на нем очутившихся, поглощен
    разглядыванием озера. Кажется, ничто на свете его не трогает.',
    hidden: true,
    loc: 'glade'
}

Пока что наш старик, как бы это сказать... не очень живой. Придется над ним еще немного поработать.

Добавим старцу метод activity:

activity: function() {
    var activityNumber = Math.round(Math.random() * 4)
    var activityText = [
        'Старик украдкой поглядывает на вас.',
        'Старик что-то бормочет себе под нос.',
        'Старый лодочник в задумчивости чешет макушку.',
        'Старик бормочет что-то о вездесущих туристах.',
        'Старец громко похрапывает.'
    ]
    p(activityText[activityNumber])
}

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

Добавим вызов activity в afterAll:

afterAll: function() {
    player.loc == 'glade' && oldman.activity()
}

Содержимое тела метода afterAll, на самом деле, условие, хотя здесь и не использован оператор if. Подобная запись частенько используется в JavaScript'е. Как вы заметили, метод activity вызывается у объекта oldman, которому он принадлежит. Вообще, полезно писать универсальные методы, которые можно вызывать из разных функций.

Теперь, если вы запустите игру, то увидите, что наш старик стал гиперактивным. На каждое наше действие выводится описание деятельности старика, что не очень-то хорошо. Добавим в наш метод условие, по которому старик будет проявлять активность только в 50% случаях.

activity: function() {
    var activityNumber = Math.round(Math.random() * 9)
    if (activityNumber < 5) {
        var activityText = [
            'Старик украдкой поглядывает на вас.',
            'Старик что-то бормочет себе под нос.',
            'Старый лодочник в задумчивости чешет макушку.',
            'Старик бормочет что-то о вездесущих туристах.',
            'Старец громко похрапывает.'
        ]
        p(activityText[activityNumber])
    }
}

12.5 Диалоги

Начну с плохой новости. Поскольку протопарсер умеет распознавать только глагол и существительное, то сложные фразы, типа, ЛЛОЙД, ОТКРОЙ ХОЛОДИЛЬНИК И ПРИНЕСИ МНЕ ПИВА он просто не поймет. В протопарсере общение игрока с персонажами реализуется через ввод ключевых слов: СКАЗАТЬ ПРИВЕТ, СПРОСИТЬ ЛОДКУ, ОТВЕТИТЬ ПАРОЛЬ, и т.д.

Давайте научим дедушку понимать и отвечать на наши вопросы. Добавим объекту oldman метод response:

response: function(optional) {
    switch(optional) {
        case 'здравствуйте':
        case 'здравствуй':
        case 'привет': var response = 'И тебе не хворать.'
            break
        case 'фантазия':
        case 'парк':
        case 'замок':
        case 'башня': var response = 'Парк-то здешний вроде заповедника
        волшебного - разными чудесами полнится. Видал дерево? А с утра
        не было. Небось твоя работа? Меня-то, люди добрые Михеем кличут,
        я здесь, вроде как, сторож, приглядываю за всем, порядок блюду,
        в смысле, блюжу, охраняю, короче.'
            break
        case 'михей': var response = 'Шо?'
            break
        case 'озеро': var response = 'Давно тут сижу. Озеро широкое, озеро
        глыбокое - вплавь не переплыть, с ахвалангом не перейтить. Вот
        только лодчонка моя могеть озеро тутошнее того... одолеть.'
            break
        default: var response = 'Чего говОришь-то?'
    }
    p('Старик отвечает: «' + response + '»')
}

Добавим вызов response в afterAll:

afterAll: function(com, obj, optional) {
    player.loc == 'glade' && oldman.activity()
    if (oldman.loc == player.loc && com == 'say') oldman.response(optional)
}

Мы разместили вызов oldman.responce() в методе afterAll, чтобы движок вызвал сперва стандартную функцию say, которая выведет сообщение «Вы говорите...», а затем выдаст ответ старика. Мы добавили функции несколько параметров. С com вы уже знакомы, obj нужен только, чтобы соблюсти последовательность передачи аргументов, а optional передает текст сказанного игроком. С этим аргументом мы и будем вызывать наш метод. Метод response, как и activity, также, не является стандартной функцией.

Обращаю внимание: если вы добавите персонажу метод say, он не сработает. Команды СКАЗАТЬ, СПРОСИТЬ, и т.п. безадресные, иначе говоря, они не завязаны на объект (персонаж). Однако, вы можете разместить метод say в той локации, в которой находится персонаж или прописать его в globalVerbs, и он будет работать как должен. И, разумеется, вы можете перехватить вызов стандартной функции в методе beforeAll, который, практически, всеяден (улыбка).

Как вы помните, наша цель – достать шляпу, которая была унесена на середину озера. Игрок может спросить у старика про шляпу, и попросить воспользоваться лодкой. Старик, разумеется, согласится нам помочь... за символическое вознаграждение.

case 'шляпа':
    case 'шляпу':
        var response = 'Шляпу-то? Видал-видал. Пролетела тут, давеча, как
        фанера над Парижем, да и в воду бултыхнулась.'
        break
    case 'монета':
    case 'монету':
    case 'деньги':
    case 'лодка':
    case 'лодку': if (coin.loc == 'oldman') var response = 'Ступай милый,
    ступай!'
        else if (coin.loc == 'player') {
            var response = 'Вот спасибо-то! Будет старику прибавка к пенсии. 
            Забирай лодку, коль не передумал.'
            move(coin, 'oldman')
        }
        else var response = 'Помоги дедушке материально, и катайся себе на
        здоровье, милок!'
        break

В зависимости от того где (или у кого) находится монета и будет зависеть ответ лодочника.

А что будет, если игрок решит пойти на север к озеру? Для начала создадим озеро.

var lake = {
    spec: 'room',
    head: 'Озеро',
    desc: 'В круглом голубом озере отражается небо. Поляна расстилается далеко на юге.',
    s: 'glade'
}

А теперь добавим нашей локации метод walk:

walk: function(obj, dir) {
    if (dir == 'n' && coin.loc != 'oldman') {
        p('«А ну отойди от лодки! Ишь, ты, хулиган!». Резкий окрик
        старика удержал вас от попытки воспользоваться лодкой.')
        return true
    }
}

Таким образом, чтобы воспользоваться лодкой (пойти на север) нам нужно, чтобы монета оказалась у старика.

Кроме возможности поговорить у нас также есть возможность показать некий предмет. При этом, предмет не обязательно должен быть у нас в инвентаре.

Предположим, игрок, решил что-то показать старику. Добавим все возможные реакции в метод afterAll:

if (com == 'show' && player.loc == 'glade') {
    if (obj == coin) {
        p('Глаза старика радостно загораются. «Вот спасибо-то! Будет старику прибавка к пенсии. Забирай лодку, коль не передумал.»')
        move(coin, 'oldman')
    } else p('Старик с видом знатока кивает: «' + capitalize(obj.nam[0]) + ' что надо!»')
}

12.6 «Виртуальные» двери и «мнимые» выходы

Хорошо, но откуда игрок возьмет монету? Если помните, у нас осталась «неиспользованной» еще одна локация – выход из парка.

var entrance = {
    spec: 'room',
    head: 'Вход в парк',
    desc: 'Широкие решетчатые ворота на севере отделяют Фантазию от внешнего
    мира. У входа в парк стоит автомат по продаже билетов. На юго-западе
    раскинулась широкая поляна.',
    sw: 'glade',
    n: ''
}

Мы оставили пустое значение n, потому что выход на север никуда не ведет (позволить игроку уйти и, таким образом, закончить игру было бы слишком просто). Однако, если бы мы вообще убрали это свойство, то при попытке игрока пойти на север игра бы отвечала «В этом направлении нельзя пойти.» Добавим ворота, и я объясню, как все это работает.

var gate = {
    spec: 'thing',
    nam: ['ворота', 'ворот', 'воротам', 'ворота', 'воротами', 'воротах', 'фигуры', 'фигуру', 'животных', 'животное', 'створки',
    'створка', 'дверь', 'двери'],
    gend: 'p',
    desc: 'Чугунные прутья ворот украшены цветным фигурами сказочных
    животных.',
    hidden: true,
    loc: 'entrance',
    door: true,
    locked: true,
    closed: true
}

В разделе «7.7 Свойства door, closed, locked» мы уже создавали объект-дверь, и помним, что свойство loc такого объекта содержит два элемента – имена локаций, которые «соединяет» дверь. Однако, в некоторых случаях, свойство loc объекта-двери может содержать всего один элемент. Например, если, выход из локации обозначен, но, фактически, никуда не ведет (свойство-направление локации равно '') и дверь (с одним элементом loc) закрыта, то при попытке игрока пройти в «мнимом» направлении, игра выдаст «Путь прегражден дверью.» Если игрок попытается двигаться в другом («реальном») направлении игра без вопросов его пропустит. Подобный способ нужно использовать с осторожность, поскольку такая «виртуальная дверь»5 будет возникать на пути игрока при попытки пройти через любой «мнимый» выход из данной локации.

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

Кроме того, необходимо убедиться, что игрок, ни при каких условиях, не сможет открыть дверь, иначе он окажется там, где его не должно быть и тем нарушит пространственно-временной континуум, и, что еще хуже, сломает игру.

Нам осталось только позаботиться, чтобы игрок не смог отпереть ворота.

unlock: function() {
    p('Вы внимательно осматриваете ворота, но ни замка, ни задвижки,
    ни другого запирающего устройства не находите. Потратив еще несколько
    минут изучая створки и пытаясь найти хоть какие-нибудь лазейки, вы пришли к выводу, что ворота явно волшебные, и просто так не откроются.')
    return true
}

12.7 Строка состояния

В разделе 4.2 Интерфейс командной строки мы познакомились с элементами командной строки. Одним из таких элементов является приглашение командной строки. По-умолчанию, оно представлено на экране символом «>». Это значение хранится в свойстве game.prompt. Вы можете установить другое значение, и, даже, менять его динамически. Последнее дает возможность организовать в командной строке строку состояния (статус-бар). Теперь, когда мы познакомились с методами и объектом events нам ничего не стоит добавить строку состояния в нашу игру.

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

var watch = {
    spec: 'thing',
    nam: ['часы', 'часов', 'часам', 'часы', 'часами', 'часах', 'пластик', 
    'ремешок'],
    desc: 'Легендарные электронные часы «Motnana» из благородного черного 
    пластика с элегантным гибким ремешком для удобного ношения. Вечная
    классика!',
    loc: 'player',
    takeable: true, 
    worn: false,
    drop: function() {
        if (player.loc == 'lake') {
            p('Пустить на дно такие часы? Ну уж нет!')
            return true
        }
    }
}

А, теперь, самое главное!

afterAll: function(com, obj, optional) {
    watch.worn ? game.prompt = new Date().toLocaleTimeString() + ' > ' : 
    game.prompt = '> '
    ...
}

Первой строкой в методе afterAll мы проверяем надеты ли часы. Если оказывается, что надеты, то вместе с символом «> » в командной строке пользователь увидит текущее время. Если игрок снимет часы, то приглашение командной строки вновь изменится, приняв стандартный вид. Мы использовали инструкцию game.prompt = '> ', потому что она достаточно короткая, но если бы вам нужно было вернуть какое-то длинное системное значение, то проще и безопаснее (поскольку, в этом случае отсутсвует риск случайного искажения начального значения) было бы это сделать с помощью инструкции game.[свойство] = defSysVal.[свойство].

Кстати, а что было бы, если бы мы поместили проверку не в метод afterAll, а в метод beforeAll? В этом случае мы увидили бы изменение строки приглашения только через один ход.

12.8 Игровой счет

Ну, вот, мы почти и подошли к финалу нашего маленького приключения. Добавим автомат по продаже билетов и монетку.

var redBox = {
    spec: 'thing',
    nam: ['автомат', 'автомата', 'автомату', 'автомат', 'автоматом',
    'автомате', 'ящик', 'щель', 'лоток', 'отверстие'],
    desc: 'Высокий с человеческий рост красный ящик чем-то неуловимо напоминает
    первые модели игровых автоматов. Рядом с щелью для купюр имеется отверстие
    откуда выходят билеты, а ниже под ним - лоток для сдачи.',
    hidden: true,
    loc: 'entrance',
    attack: function(obj, optional, verb) {
        if ((verb == 'ударить' || verb == 'пнуть' || verb == 'стукнуть') 
        && !redBox.beaten) {
            p('Вы со всей силой ударяете по автомату, который тут же, в
            отместку, выплевывает в вас серебряную монету.')
            move(coin, 'entrance')
            redBox.beaten = true
            reward(5)
            return true
        }
    },
    beaten: false
}

Этот пример требует пояснений. Во-первых, мы вызвали функцию attack с несколькими аргументами. В действительности, из них нам нужен только verb: глагол, который употребил пользователь. Функция attack «активируется» при вводе разных команд (полный список можно посмотреть в Приложении 1 «Стандартные функции-обработчики (API)»). Однако, нам нужно, чтобы функция срабатывала при вводе только некоторых из них: УДАРИТЬ, ПНУТЬ И СТУКНУТЬ.

Во-вторых, мы добавили объекту redBox свойство (флаг) beaten. До этого мы добавляли объектам пользовательские методы, и использовали стандартные свойства. Однако, вы так же можете добавлять объекту любые новые свойства. Теперь, если игрок попытается ударить автомат еще раз, функция не сработает.

В-третьих, мы использовали стандартную функцию reward, которая изменяет игровой счет и информирует об этом пользователя. Эта функция принимает в качестве единственного аргумента число, причем, как положительное, так и отрицательное. Если аргумент больше нуля пользователь увидит сообщение «Ваш счет увеличился на <значение аргумента>!» и далее число сделанных ходов и счет, с учетом значения аргумента. Если аргумент меньше нуля текст сообщения будет иным: «Ваш счет уменьшился на <значение аргумента>!».

По-умолчанию, счет на начало игры равен нулю. Однако, вы можете изменить это значение, добавив в объект game свойство points с необходимым значением начального счета. Аналогичным образом вы можете установить начальное значение хода (свойство turn объекта game).

Если в вашей игре не ведется счет установите game.noScore: true. Информация о счете и количестве ходов нигде не появится.

Значения текущего счета и количества ходов хранятся в свойствах game.points и game.turn. Вы можете получить значение текущего хода, даже если game.noScore равно true, поскольку протопарсер в конце каждого хода автоматически увеличивает счетчик ходов.

Вы можете подогреть интерес пользователя к игре, если укажите максимальный счет, который он может достичь. Для этого добавьте в объект game свойство maxScore:

maxScore: 5

Теперь, если пользователь введет команду СЧЕТ, то увидит «К <Х> ходу ваш счет равен <Y> из <Z>

Добавим в нашу игру монетку:

var coin = {
    spec: 'thing',
    nam: ['монета', ' монеты', 'монете', 'монету', ' монетой', 'монете',
    'деньги'],
    desc: 'Маленькая серебряная монетка с надписью «Жетон Банка Фантазии».',
    loc: '',
    takeable: true
}

Теперь, когда у нас есть монета мы можем вернуться на поляну и воспользоваться лодкой, предварительно ее создав:

var boat = {
    spec: 'thing',
    nam: ['лодка', 'лодки', 'лодке', 'лодку', 'лодкой', 'лодке', 'краску'],
    desc: 'Старое корыто, коим, по-сути, и была лодка неизвестно каким чудом
    держалась на воде. Краска на ее бортах давно потрескалась и отлетела.
    Внутри места хватало ровно на одного человека, да еще на бутылку в которую
    можно положить послание «Я был так глуп, что решил отправиться в плавание
    на этой скорлупе. Не повторяйте моей ошибки».',
    hidden: true,
    loc: ['glade', 'lake']
}

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

Поместим следующий код в beforeAll:

if ((player.loc == 'glade' && optional == 'n' && coin.loc == 'oldman') ||
   (player.loc == 'lake' && optional == 's')) 
    p('Вы усиленно нажимаете на весла, и в несколько мощных гребков
    преодолеваете половину озера.')

Добавим еще несколько штрихов к объекту lake:

walk: function(obj, dir) {
    if (dir != 's' && dir != 'u') {
        if (dir == 'd') p('У вас нет акваланга.')
        else p('Сейчас не время кататься по озеру.')
        return true
    }
}

Есть одна вещь о которой можно легко забыть. Ведь мы с вами на озера, а у игрока может быть при себе ключ. Что произойдет, если он решит БРОСИТЬ КЛЮЧ. Если мы специально не прописали обработчики для этого случая, то результат будет несколько странным.

> БРОСИТЬ КЛЮЧ
Вы оставляете здесь ключ.
> ОСМ
В круглом голубом озере отражается небо. Поляна расстилается далеко на юге. Здесь есть: ключ, шляпа.

Ну, со шляпой все понятно – она плавает на поверхности озера. А, вот, бронзовый ключ, пожалуй, бы должен пойти ко дну. Исправим это:

drop: function() {
    if (player.loc == 'lake') {
        remove(this)
        p('Бульк!<br>Ключ быстро исчезает в мутной воде.')
        return true
    }
}

Возможно, требует пояснения тэг <br>. Это стандартный HTML-элемент, с помощью которого мы вывели следующее за «Бульк!» предложение с новой строки. Подробнее о HTML-тэгах и форматировании будет рассказано в главе «Форматирование вывода».

12.9 Завершение игры

Вот мы и подошли к финалу нашей истории. Игрок доплыл до середины озера, взял шляпу, прочитал текст на ленте. Он уже примеривает на себе головной убор, и набирает на клавиатуре заветное слово... Нам остается добавить несколько строчек кода в метод afterAll:

if (com == 'say' && optional == 'домой' && hat.worn) {
    p('Едва вы произнесли это слово, как, внезапно, обнаружили, что
    сидите уставившись в экран. Ха-ха! Это была всего лишь игра. Спасибо,
    что уделили ей время. Приходите еще!')
    end(1)
}

Ваша игра может закончиться хорошо (как наша) или не очень, или вообще не закончиться, если в ваш код вкралась ошибка. Функция end определяет где и как завершится ваша игра. Вы можете вставить ее в любое место в своем коде. Но, как только до нее дойдет очередь игра будет тут же остановлена. После остановки игры командная строка вместе с курсором исчезнут, и пользователь уже ничего не сможет ввести. Вы можете явно сообщить игроку об окончании игры, если вызовите функцию end с аргументом: 0 – если игрок проиграл, 1 – если выиграл. Если game.noScore не равно true, пользователь увидит итоговое количество сделанных им ходов и число набранных очков.

Если вы вызовите end без аргументов, на экран ничего выведено не будет, и игра просто закончится.

Глава 13

Сохранение и загрузка

В протопарсере реализован механизм мульти-сохранения и загрузки игр. Для корректной работы этой функции необходимо, чтобы браузер предоставлял приложению доступ к объекту localStorage. Именно в нем хранятся сохраненные игры. При сохранении, в localStorage попадают все объекты со свойством spec. Когда вы пишите игру это свойство необходимо добавлять только объектам типа room и thing. Для прочих стандартных объектов движок добавит его сам. Имена объектов (key) являются свойствами localStorage, а сами объекты (value) – его значениями. Имена игровых объектов, хранящихся в localStotage, формируются по следующему шаблону: <имя объекта>___<имя файла сохранения>___<имя игры>.

При старте игры протопарсер автоматически сохраняет начальное состояние игровых объектов под именем <имя объекта>___initialState___<имя игры>. В дальнейшем, если пользователь введет команду ЗАНОВО, движок загрузит объекты из свойств localStorage с этим именем.

Чтобы загрузить сохраненную игру необходимо запустить соответствующую игру и ввести команду ЗАГРУЗИТЬ <ИМЯ ФАЙЛА СОХРАНЕНИЯ>. «Имя файла сохранения» должно состоять из одного слова. Вы можете сохранять разные игры под одним названием. Имеются в виду игры с разными названиями, т.е. с разными значениями game.title.

Большинство современных браузеров позволяют переключаться в т.н. «режим разработчика». В этом режиме вы можете просматривать состояние сохраненых игровых объектов (переменных) в localStorage, что может быть полезно при отладке игры.

Чтобы воспользоваться этой функцией:

Вы можете запретить пользователю восстанавливать и сохранять игру. Для этого установите свойство noSaveLoad объекта game равным true.

var game = {
    noSaveLoad: true
}

Если в вашей игре запрет на загрузку/сохранение нужен только в определенные моменты, вы можете добавить в свой код инструкцию game.noSaveLoad = true для установки запрета и game.noSaveLoad = false – для отмены.

Глава 14

Тестирование и отладка

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

Откройте файл tests.js. Команды для тестирования последовательно записываются в массив commands. Для примера, в массиве уже есть несколько команд. Вы можете удалить их и добавить свои. После этого в файле story.js установите свойство game.tests равным true.

Запустите файл index.html. Тестирование начнется сразу после вывода описания стартовой локации (если есть пользовательские методы, то они тоже сработают). Далее программа будет последовательно вводить команды из массива commands и выводить результат их выполнения на экран. По окончании тестирования появится сообщение «ТЕСТИРОВАНИЕ ЗАВЕРШЕНО.» Вы можете продолжить вводить команды как обычно.

Вы также можете проверить корректность работы протопарсера на вашем устройстве, запустив из папки examples/Fantasia файл index.html с включенным режимом автотестирования.

Благодаря наличию в современных браузерах «режима разработчика», вы можете в режиме реального времени просматривать и изменять значение свойств игровых объектах. Это бывает особенно полезно при дебаггинге.

Чтобы воспользоваться этой функцией:

Иногда в коде встречаются ошибки. Когда интерпретатор js встречает такую ошибку он может просто ее проигнорировать, либо прекратить выполнение. В обоих случаях, игра, на каком-то этапе, может повести себя не так как мы ожидали. И, что еще хуже, браузер не сообщит нам какую ошибку он встретил и где. Однако, все-же есть простой и удобный способ это выяснить. Как вы догадались, это уже знакомый нам «режим разработчика».

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

Вы, наверное, уже поняли, что «режим разработчика» является практически заменой традиционного IDE. Фактически, вы можете писать игру в браузере. Выбирайте способ, который вам кажется проще и удобнее.

Глава 15

Настройка стилей элементов игрового окна

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

Как бы то не было, являетесь ли вы последователем «старой школы» или вам по душе творческие эксперименты, в протопарсере вы можете полностью настроить «внешний вид» игры «под себя». Для этого всего лишь нужно отредактировать файл стилей style.css.

Основные элементы игрового окна приведены в следующей таблице:

Название элемента Описание
#terminal Окно терминала
#output Область игрового вывода
.userCommand Строки, содержащие команду пользователя
#prompt Область значка приглашения
#input Окно ввода команд
#form Форма ввода
#inputLine Строка ввода (включает #prompt, #input и #form)

Каскадные таблицы стилей (Cascading Style Sheets, CSS) – мощный инструмент в руках веб- и гейм-дизайнера. Вообще, материала по этой теме хватит не на одну книгу, поэтому в данной главе я ограничусь несколькими примерами использования стилей. Впрочем, вероятно, вам и не понадобится знать весь синтаксис CSS, чтобы сделать некоторые простые вещи.

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

body {
    color: black;
    background-color: white;
}

Как видите, свойства стиля очень похожи на свойства объекта.

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

if (player.loc == 'hall' || player.loc == 'round') {
    document.body.style.color = 'white'
	document.body.style.backgroundColor = 'black'
    document.getElementById('input').style.color = 'white'
} else {
    document.body.style.color = 'black'
	document.body.style.backgroundColor = 'white'
    document.getElementById('input').style.color = 'black'
}

Глава 16

Форматирование вывода

В предыдущей главе мы рассмотрели лишь крохотную часть тех возможностей, которые предоставляет CSS. Помимо установления «глобальных стилей» вы можете задавать форматирование для отдельных участков текста. Давайте сделаем красочное оформление для нашего билета. Для этого изменим свойство text объекта ticket:

text: '<div style="color:yellow; background-color: blue;"><p style="text-align: center;font-weight: bold;">
«Уважаемый посетитель # 3677!</p><p style="font-style:italic;">Мы рады приветствовать вас в Фантазии. 
Пожалуйста, ни чему не удивляйтесь и чувствуйте себя как дома. Но, если вам и впрямь надо вернуться домой используйте шляпу».</p></div>',

Здесь, для задания стилей отдельных элементов мы использовали HTML-атрибут style. Прежде всего, мы поместили весь текст в элемент (тэг) <div>, чтобы установить цвет всего текста желтым (color:yellow), а фон – голубым (background-color: blue). Дальше мы разбили текст на абзацы (элемент <p>). Первый абзац (заголовок) мы сделали полужирным и выровняли по центру:

<p style="text-align: center;font-weight: bold;">

Второй абзац мы сделали курсивным.

<p style="font-style:italic;">

Вы можете вставлять в текст ссылки, например:

info: '<a href="https://ifiction.ru"
target="_blank" style="color: brown">Форум любителей интерактивной литературы</a>'

Мы добавили в тэг <a> атрибут target, чтобы при нажатии на «protoparser.js» ссылка открылась в новом окне. Мы также изменили цвет ссылки на коричневый для того, чтобы она лучше читалась, но, при этом, не слишком отвлекала на себя внимание. Вы можете устанавливать на ссылки события, прятать их, и т.п. Таким образом, можно, например, сделать в своей игре главное меню.

В этой главе мы использовали т.н. «встроенные стили». Вы можете встраивать в свою игру не только стили, а вообще любые элементы: картинки, музыку, формы, и, даже таблицы.

Глава 17

Мультимедийные элементы и дополнительные библиотеки

Если вы чувствуете, что ваша игра выиграет, если в ней будут картинки и музыка – эта глава для вас.

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

info: '<a href="https://ifiction.ru"
target="_blank" style="color: brown">Форум любителей интерактивной литературы</a>
<img src="line.jpg">'

Если вы предпочитаете складывать картинки не в корневую директорию, а в отдельную папку, например, pics, то путь будет иной:

<img src="pics/line.jpg">

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

Таким же образом, через установку соответствующих атрибутов, вы можете добавить в игру музыку. Для этого добавим тэг <embed> в конец info.

info: '<a href="https://ifiction.ru"
target="_blank" style="color: brown">Форум любителей интерактивной литературы</a>
<embed src="Sound.mp3" hidden="true"></embed>'

В приведенном примере музыкальный трек запустится сразу при старте игры. У тэга <embed> довольно много атрибутов. В нашем примере мы скрыли плеер (hidden="true"), но вы можете и оставить его, тем самым дав возможность игроку изменить громкость или остановить воспроизведение. В атрибутах можно задавать уровень громкости, количество повторений трека, и т.д. Разумеется, вы можете запускать музыку или звуки при наступлении какого-то события. Следующий пример показывает, как можно сделать скрип двери при открытии:

open: function() {
    var doorCreak = new Audio('creak.mp3')
    doorCreak.play()
}

В своей игре вы можете использовать многочисленные js-библиотеки. Чтобы подключить необходимую библиотеку пропишите путь к ней в заголовке файла index.html:

<script src="mySuperExtension.js"></script>

, если файл mySuperExtension.js расположен в корне папки с игрой, либо:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

, если необходимо импортировать библиотеку из внешнего файла.

Глава 18

Создание новых и изменение параметров стандартных команд. Виртуальные функции

Вообще говоря, каждая команда состоит из двух частей: параметров, определяющих вызов командной функции и самой функции, определяющей поведение команды. Все стандартные команды единственное назначение которых – вывести статичный текст являются «виртуальными», т.е., у них есть параметры, определяющие то, как данная команда будет «вызываться», но сама функция, фактически, отсутствует. Ее роль выполняет свойство объекта game, имеющее имя «виртуальной» функции и содержащее текст стандартного сообщения, определенного для данной команды. Это порождает два следствия: во-первых, такую «функцию» нельзя вызвать из своей программы. Вместо этого следует обращаться к соответствующему свойству объекта game. Во-вторых, если вы планируете создать «реальную» функцию взамен «виртуальной» вам необходимо установить false в качестве значения соответствующего свойство объекта game и создать определение самой функции.

Для примера разберем команду ПОМОЩЬ. Соответствующая этой команде функция help является «виртуальной». Допустим, мы хотим, чтобы при каждом вызове игроком «помощи» его счет уменьшался бы на 1. Для этого мы должны сделать функцию «реальной».

var game = {
    ...
    help: false
}

function help() {
    p(defSysVal.help) // берем текст сообщения из значений по-умолчанию
    game.points--
} 

С функциями, которые определяют то, как должна себя вести команда мы познакомились в Главе 9. Методы объектов. До сих пор мы использовали стандартные параметры вызова команд, заменяя лишь стандартные функции своими.

Параметры команд хранятся в объекте gameCommands. Набор параметров каждой команды является свойством объекта gameCommands и представляет собой массив значений.

Для примера давайте разберем параметры команды take (ВЗЯТЬ).

take: ['take', ['взять', 'поднять', 'забрать', 'подобрать'], 1]

Как видно из примера, свойству take соответствует массив из 3-ех элементов.

В большинстве случаев, название свойства соответствует названию соответствующей функции (метода), но есть несколько исключений. Вы можете уточнить стандартные имена свойств и методов в Приложении 1. Если вы будете создавать новую команду желательно, чтобы название свойства совпадало с названием метода.

Итак, рассмотрим параметры команды из примера.

Первый элемент – имя вызываемой функции. Само-собой, функция с таким именем должна существовать, если только она не «виртуальна». Но, в таком случае, мы должны определить для объекта game свойство с данным именем. Значением свойства будет текст, которой мы, который мы хотим выводить на экран, когда пользователь вводит соответствующую команду.

Второй элемент, как вы заметили, это массив. В нем хранятся глаголы-синонимы для данной команды. Если пользователь введет один из этих глаголов (совместно с существительным, в данном примере), то будет вызвана функция, указанная в первом элементе (take). Имена глаголов следует вводить строчными буквами. Очень важно, чтобы глаголы не совпадали с уже существующими. Список глаголов-синонимов каждой команды указан в Приложении 1 в той последовательности в которой они располагаются в массиве соответствующего свойства.

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

В нашем примере этот параметр имеет значение 1, т.е. вместе с глаголом обязательно должно стоять существительное (например, ВЗЯТЬ ЯБЛОКО), причем соответствующий объект (яблоко) должен находиться в той же локации, что и персонаж. Если какое-либо из условий не будет соблюдено функция take не будет выполнена, а интерпретатор сообщит пользователю о допущенной при вводе ошибке.

Кроме трех представленных в примере параметров возможно указать дополнительный параметр, который будет передаваться в функцию при вызове. В качестве параметра можно указать любое значение, однако существует несколько специальных «зарезервированных» значений. Если установить в качестве параметра значение 'noun' в функцию, в качестве дополнительного параметра, будет передано существительное, которое ввел пользователь, а если 'verb', то глагол. Существует еще специальное значение sysCom – идентификатор системной команды (метакоманды). При вызове метакоманды счетчик ходов не увеличивается.

Попробуем немного изменить нашу команду, «научив» ее глаголу СХВАТИТЬ. Для этого добавим в свойство init объекта events следующую инструкцию:

gameCommands.take[1].push('схватить')

Поскольку массив глаголов является вторым элементом массива параметров, т.е. имеет индекс «1» мы указываем его возле имени свойства. Метод push добавляет наш глагол к уже существующим. Методу можно передать сразу несколько значений:

gameCommands.take[1].push('схватить', 'завладеть')

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

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

gameCommands.sneeze = ['sneeze', ['чихать', 'чихнуть'], 0]

Как и в случае с командой take мы разместили нашу инструкцию в методе init. Наша команда не предполагает существительного, поэтому третьим параметром мы указываем «0». Теперь осталось добавить саму функцию.

function sneeze() {
    p('– Апч-хи!!!')
}

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

В случаях, когда после ввода команды должно выводиться только статичное сообщение можно обойтись без создания функции, добавив в объект game свойство с именем «виртуальной» функции.

var game = {
    sneeze: '– Апч-хи!!!'
}

Глава 19

Сниппеты

Сниппеты – это небольшие кусочки кода, которые можно использовать в своей программе (иногда, после небольшой адаптации). В этой главе собраны несколько полезных и интересных, на мой взгляд, сниппетов, которые могут сделать вашу игру более «живой» и интересной. Возможно, некоторые из этих сниппетов или их аналоги когда-нибудь станут частью движка, возможно – нет. Так или иначе, ничто не мешает вам прямо сейчас использовать их в вашей игре.

19.1 Все или ничего?

Многие искушенные в парсерах игроки любят использовать команду ВЗЯТЬ ВСЕ. Это, действительно, удобно, когда можно взять сразу несколько предметов одной короткой командной. К тому же, у некоторых предметов нередко бывают такие длинные названия, что проще написать ВСЕ. Кроме того, с помощью такой команды можно быстро проверить все доступные в локации предметы. Некоторые авторы позволяют игроку использовать команды типа ВЗЯТЬ ВСЕ, другие заставляют указывать конкретные объекты.

В протопарсере пока не реализована корректная обработка команд с существительным «все», однако вы можете добавить в свою игру такую реализацию самостоятельно. Более того, вы можете определить как именно движок будет обрабатывать эту команду.

Ниже приведен пример одного из вариантов такой реализации. В нем мы добавим дополнительные проверки в метод beforeAll и определяем существительное «все» (объект all) у которого будет два «популярных» метода: take и drop. В первом случае мы попросим у игрока уточнить, что именно он хочет взять, а во втором – попытаемся сразу выполнить его команду.

var events = {
    beforeAll: function(com, obj) {
        if (obj === all) {
            if (com === 'drop') {
                var inventory = getObjByKV('loc', 'player')
                if (inventory.length > 0) {
                    for (var i in inventory)
                        inventory[i].drop ? inventory[i].drop() : drop(inventory[i]) // если у объекта определен метод drop вызываем его; если нет – вызываем стандартную функцию 
                } else p(game.playerHasNothingMsg)
            } else p('Ваш взгляд жадно прыгает с предмета на предмет, но вам нужно выбрать что-то одно.')
            return true
        }
    }
}

var all = {
    spec: 'thing',
    nam: ['все', 'всего', 'всему', 'все', 'всем', ' всем', 'всё'],
    hidden: true,
    loc: ['loc1', 'loc2', 'loc3']
}          

Приложение 1

Стандартные функции-обработчики (API)

Стандартная функция-обработчик6 Аргумент(ы) функции-обработчика (тип, описание)7 Вызывающая команда Горячая клавиша по-умолчанию Описание действия Имя метода-перехватчика / имя свойства в объектах gameCommands и game Тип команды (функции)
[about()] «–» версия Alt + V Выводит значение game.about. about Метакоманда







[advice()] «–» совет, подсказка, спойлер Alt + A Выводит значение game.advice. advice Метакоманда







attack(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем ударить, пнуть, стукнуть, атаковать «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.» attack Игровая команда







bow([arg1]) arg1 (объект): объект для действия поклониться, кланяться «–» Если arg1 задан выводит сообщение «Вы кланяетесь <arg1>.», иначе – «Вы кланяетесь.» bow Игровая команда







burn(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем зажечь, поджечь, сжечь, жечь «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.» burn Игровая команда







buy(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем купить, приобрести «–» Выводит сообщение «Вы не можете <arg3> <arg1>.» buy Игровая команда







capitalize(arg1) arg1 (строка): строка для обработки «–» «–» Возвращает переданную ей строку arg1 в которой первая буква переведена в верхний регистр. «–» Служебная функция







clearScreen() «–» очистка Alt + C Очищает экран от текста. Вызывает метод clearScreen(). Очистка экрана не влияет на игровой лог. clearScreen Метакоманда







close(arg1) arg1 (объект): объект для действия закрыть «–» 1. Если у arg1 нет свойства closed выводит сообщение «<arg1> невозможно закрыть.» и завершает выполнение.
2. Если свойство closed arg1 равно true выводит сообщение «<arg1> уже закрыт.» и завершает выполнение.
3. Устанавливает свойство closed arg1 равным true и выводит сообщение «Вы закрываете <arg1>.»
close Игровая команда







cut(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем резать, разрезать, перерезать, отрезать, срезать, обрезать, пилить, отпилить, перепилить, рубить, срубить, отрубить, отсечь «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.» cut Игровая команда







disrobe(arg1) arg1 (объект): объект действия снять «–» 1. Если свойство worn arg1 не равно true выводит сообщение «На вас нет <arg1>.» и завершает выполнение.
2. Устанавливает свойство worn arg1 равным false.
3. Устанавливает свойство loc arg1 равным player.
4. Выводит сообщение «Вы снимаете <arg1>.»
disrobe Игровая команда







drop(arg1) arg1 (объект): объект для действия бросить, выбросить, положить, оставить, выкинуть «–» 1. Если среди свойств loc arg1 есть player.loc переходит к п.6.
2. Если loc arg1 не равно player переходит к п.6.
3. Если свойство worn arg1 равно true, вызывает функцию disrobe(arg1)
4. Вызывает функцию move(arg1, player.loc)
5. Выводит сообщение «Вы оставляете здесь <arg1>.» и завершает выполнение.
6. Выводит сообщение «<arg1> уже здесь.» и завершает выполнение.
drop Игровая команда







eat(arg1) arg1 (объект) – объект для действия съесть, есть, кушать, скушать «–» Если у arg1 есть свойство edible, равное true объект удаляется из игры и выводится сообщение «Вы съедаете <arg1>.». В противном случае выводит строку «<arg1> не годится в пищу.» eat Игровая команда







end([arg1]) arg1 (число): режим окончания игры: 0 – проигрыш, 1 – победа «–» «–» 1. Если arg1 равен 1 выводит значение game.winMsg.
2. Если arg1 равен 0 выводит значение game.lostMsg.
3. Если свойство game.noScore отсутствует или равно false вызывает функцию score.
4. Устанавливает значение game.stopped равным true.
5. Выводит ссылку для скачивания лога.
«–» Служебная функция







examine([arg1]) arg1 (объект): объект для действия осмотреться, осмотреть, изучить, смотреть, см, исследовать, рассмотреть, о, осм Numpad 5 1. Если arg1 задан переходит к п.5.
2. Выводит значение свойства desc текущей локации.
3. Для всех объектов, свойство loc (или один из элементов этого свойства) которых равно player.loc, и у которых отсутствует свойство hidden или оно равно false:
- если у объекта есть свойство sceneDesc добавляет его значение к описанию локации;
- если у объекта отсутствует свойство sceneDesc добавляет значение свойства nam[0] в строку «Здесь есть: » в описании локации.
4. Завершает выполнение.
5. Проверяет есть ли у аргумента свойство examined.
6. Если свойства нет – устанавливает его равным 1.
7. Если свойство есть – увеличивает его на 1.
8. Выводит значение свойства desc arg1.
examine Игровая команда







gendDef() «–» «–» «–» Вызывается как метод у объектов типа thing и player. Если у объекта не определено свойство gend определяет его, сверяя последний символ свойства nam[0] объекта со значениями свойств объекта GEND_SUFFIX. «–» Служебная функция







getObjByKV(arg1, arg2) arg1 (строка): имя свойства; arg2 (строка, число, логическое значение, null, undefined): значение свойства «–» «–» Возвращает массив объектов с заданным значением свойства. Функция ищет значение, даже если оно содержится в массиве. Если значением свойства является массив, то при проверке соответствия регистр не учитывается. Если объектов с заданной парой имя-свойство не найдено возвращает пустой массив. «–» Служебная функция







[help()] «–» помощь, справка, ? Alt + ? Выводит значение game.help. help Метакоманда







history() «–» история Alt + H 1. Если game.commandHistory[0] содержит команду – выводит список команд, сохраненных в истории команд (game.commandHistory), иначе переходит к п. 2
2. Если game.commandHistoryLength равно нулю выводит значение game.commandHistoryOffMsg, иначе – значение game.commandHistoryIsEmptyMsg.
history Метакоманда







inventory() «–» инвентарь, инв, и Numpad / 1. Функция создает массив объектов inv со свойством loc: 'player'
2. Из массива inv создается новый массив с таким же именем, который состоит только из объектов у которых отсутствует свойство hiddenPossession, либо оно равно false.
3. Если длина массива равна 0, выводит значение game.playerHasNothingMsg и завершает выполнение. В противном случае переходит к п.4
4. Выводит значение game.playerHasMsg и далее перечисляет первые элементы свойства nam элементов массива inv. Если элемент имеет свойство worn: true добавляет после его названия «(надет)».
inventory Игровая команда







isNounValid([arg1]) arg1 (строка): один из элементов свойства nam объекта «–» «–» Возвращает true, если объект, одним из элементов свойства nam которого является arg1, присутствует в инвентаре, либо в текущей локации. «–» Служебная функция







[jump()] «–» прыгать, прыгнуть, подпрыгнуть «–» Выводит значение game.jump. jump Игровая команда







jumpOver(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем перепрыгнуть «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.». jumpOver Игровая команда







[kiss()] «–» поцеловать, целовать, расцеловать «–» Выводит значение game.kiss. kiss Игровая команда







[listen([arg1]]) arg1 (объект): объект для действия слушать, подслушать, послушать «–» Выводит значение game.listen. listen Игровая команда







loadGame(arg1, arg2, arg3, arg4) arg1 (объект): undefined; arg2(строка): sysCom; arg3(строка): глагол, введенный пользователем; arg4 (строка): имя под которым была сохранена игра загрузить, восстановить «–» 1. Если значение game.noSaveLoad не равно true переходит к п.2, иначе – к п.5.
2. Вызывает метод load(arg4).
3. Если метод load вернул true выводит значение game.loadedMsg, выводит значение свойства head текущей локации (если есть), вызывает функцию examine и завершает выполнение.
4. Если метод load вернул false выводит значение game.notLoadedMsg и завершает выполнение.
5. Выводит значение game.loadForbiddenMsg.
loadGame Метакоманда







lock(arg1) arg1 (объект): объект для действия запереть «–» 1. Если у arg1 отсутствует свойство locked выводит сообщение «<arg1> невозможно запереть.» и завершает выполнение.
2. Если свойство closed arg1 равно false выводит сообщение «<arg1> не закрыт.» и завершает выполнение.
3. Если свойство locked arg1 равно true выводит сообщение «<arg1> уже заперт.» и завершает выполнение.
4. Устанавливает свойство locked arg1 равными true и выводит сообщение «Вы запираете <arg1>.»
lock Игровая команда







log() «–» лог, транскрипт Alt + J Выводит ссылку для скачивания лога. log Метакоманда







move(arg1, arg2) arg1 (объект): объект, который необходимо переместить; arg2 (строка): идентификатор локации в которую необходимо переместить объект «–» «–» 1. Если arg2 равен 'player' и значение свойства spec arg1 равно 'thing' вызывает функцию getObjByKV(arg2, 'player'), иначе переходит к п.3.
2. Если значение length массива, полученного в п.1 равно значению player.maxCarried выводит значение game.overburdenMsg и возвращает false, иначе переходит к п.3.
3. Устанавливает значение свойства loc arg1 равным arg2, значение свойства moved arg1 – равным 1, если оно отсутствует, а в противном случае увеличивает его на 1
4. Если arg1 равен player и значением свойства loc arg2 является 'room' переходит к п.5, иначе переходит к п.8.
5. Если у arg2 нет свойства visits добавляет его и инициализирует его значением 1, если есть – увеличивает его на 1.
6. Если у локации есть свойство head выводит его значение.
7. Вызывает функцию examine.
8. Возвращает true.
«–» Служебная функция







open(arg1) arg1 (объект): объект для действия открыть «–» 1. Если у arg1 отсутствует свойство closed выводит сообщение «<arg1> невозможно закрыть.» и завершает выполнение.
2. Если свойство closed arg1 не равно true выводит сообщение «<arg1> уже открыт.» и завершает выполнение.
3. Если свойство locked arg1 равно true выводит сообщение «<arg1> заперт.» и завершает выполнение.
4. Устанавливает свойство closed arg1 равными false и выводит сообщение «Вы открываете <arg1>.»
open Игровая команда







p(arg1) arg1 (строка): текст для вывода на экран «–» «–» Выводит <arg1> на экран. «–» Служебная функция







places() «–» места, локации, комнаты Alt + M 1. Если у объекта visitedLocs отсутствуют свойства (пустой объект) переходит к п.4.
2. Выводит строку «Вы посетили:» и далее список значений свойства head объектов идентификаторы которых являются свойствами visitedLocs (посещенные локации).
3. Для каждого свойства объекта visitedLocs: если значение свойства не пустое, выводит под названием локации строку «Вы осмотрели: » и, далее, значения элементов массива, являющегося значением соответствующего свойства. После чего завершает выполнение.
4. Выводит значение game.noVisitedMsg.
places Метакоманда







read(arg1) arg1 (объект): объект для действия читать, прочитать «–» Если объект имеет свойство text, выводит значение этого свойства. В противном случае, выводит строку «На <arg1> ничего не написано.» read Игровая команда







remove(arg1) arg1 (объект): объект для действия «–» «–» Удаляет объект из игры (присваивает пустое значение локации и падежным формам объекта). Доступ к объекту сохраняется в обрабатывающей функции до тех пор, пока интерпретатор не дойдет до инструкции return либо последней инструкции в функции. Будьте внимательны, обращение к свойству несуществующего объекта вызовет ошибку TypeError. «–» Служебная функция







repeat() «–» п, повтор, повторить Numpad + Если game.commandHistory[0] содержит команду выполняет ее, иначе – вызывает функцию history. repeat Метакоманда







restart() «–» заново, сначала «–»
Выводит значение game.confirmRestartMsg. Если пользователь нажимает OK перезагружает страницу с игрой. Иначе — выводит значение game.cancelRestartMsg.
restart Метакоманда







reward([arg1]) arg1 (число): величина на которую изменяется значение game.points «–» «–» 1. Если значение game.noScore равно true функция завершает свою работу.
2. Функция проверяет является ли arg1 числом. Если не является – функция завершает свою работу.
3. Если arg1 больше нуля выводит сообщение «Ваш счет увеличился на <arg1>.»
4. Если arg1 меньше нуля выводит сообщение «Ваш счет уменьшился на <arg1>.»
5. К значению game.points прибавляется значение arg1.
6. Вызывается функция score.
«–» Служебная функция







rub(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем тереть, протереть, натереть, начистить, потереть «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.» rub Игровая команда







saveGame(arg1, arg2, arg3, arg4) arg1 (объект): undefined; arg2(строка): sysCom; arg3(строка): глагол, введенный пользователем; arg4 (строка): имя под которым будет сохранена игра сохранить «–» 1. Если значение game.noSaveLoad не равно true переходит к п.2, иначе – к п.5.
2. Вызывает метод save(arg4).
3. Если метод save вернул true выводит значение game.savedMsg и завершает выполнение.
4. Если метод save вернул false выводит значение game.notSavedMsg и завершает выполнение.
5. Выводит значение game.saveForbiddenMsg.
saveGame Метакоманда







say(arg1, arg2) arg1 (объект): undefined; arg2 (строка): слово сказать, произнести, ответить, спросить, говорить «–» Выводит сообщение «Вы говорите: «<arg2>».» Обратите внимание, arg2 содержит только одно (первое) слово, независимо от того, сколько слов после команды СКАЗАТЬ ввел пользователь. say Игровая команда







score() «–» счет, счёт, сч, очки, ход, ходы Numpad * 1. Если game.noScore равно true выводит значение game.noScoreMsg.
2. Иначе, если свойство game.maxScore отсутствует или равно false выводит сообщение «К <game.turn> ходу ваш счет равен <game.points>.»
3. Иначе выводит сообщение «К <game.turn> ходу ваш счет равен <game.points> из <game.maxScore>.»
score Метакомандаа







screw(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем завинтить, закрутить, вкрутить, ввинтить, прикрутить «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.» screw Игровая команда







sell(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем продать, сбыть «–» Выводит сообщение «Вы не можете <arg3> <arg1>.» sell Игровая команда







show(arg1) arg1(объект): объект для действия показать, продемонстрировать, демонстрировать «–» Если свойство loc arg1 равно 'player' выводит сообщение «Вы показываете <arg1>.», иначе выводит сообщение «Вы показываете на <arg1>.» show Игровая команда







[sing()] «–» петь, спеть, запеть «–» Выводит значение game.sing. sing Игровая команда







[sleep()] «–» спать, поспать, заснуть «–» Выводит значение game.sleep. sleep Игровая команда







[smell([arg1])] arg1 (объект): объект для действия нюхать, понюхать «–» Выводит значение game.smell. smell Игровая команда







take(arg1) arg1 (объект): объект для действия взять, поднять, забрать, подобрать «–» 1. Если свойство takeable arg1 не равно true выводит сообщение «Вы не можете взять <arg1>» и завершает выполнение.
2. Если среди элементов свойства loc arg1 есть элемент 'player' выводит сообщение «<arg1> уже у вас.» и завершает выполнение.
3. Вызывается функция move(arg1, 'player'), и, если она возвращает true выводит сообщение «Вы забираете <arg1>.»
take Игровая команда







[think()] «–» думать, подумать, размышлять, задуматься «–» Выводит значение game.think think Игровая команда







tie(arg1, arg2, arg3) arg1 (объект): объект для действия; arg2 (строка): undefined; arg3 (строка): глагол, введенный пользователем привязать, завязать, связать «–» Выводит сообщение «Не стоит пытаться <arg3> <arg1>.» tie Игровая команда







unlock(arg1) arg1 (объект): объект для действия отпереть «–» 1. Если у arg1 нет свойства locked выводит сообщение «<arg1> невозможно отпереть.» и завершает выполнение.
2. Если свойство closed arg1 не равно true выводит сообщение «<arg1> уже открыт.» и завершает выполнение.
3. Если свойство locked arg1 не равно true выводит сообщение «<arg1> не заперт.» и завершает выполнение.
4. Устанавливает свойство locked arg1 в значение false и выводит сообщение «Вы отпираете <arg1>.»
unlock Игровая команда







unscrew(arg1) arg1 (объект): объект для действия открутить, выкрутить, вывинтить «–» Выводит сообщение «<arg1> ни к чему не прикручен.» unscrew Игровая команда







untie(arg1) arg1 (объект): объект для действия развязать, отвязать «–» Выводит сообщение «<arg1> ни к чему не привязан.» untie Игровая команда







[wait()] «–» ждать, подождать, ж «–» Выводит значение game.wait. wait Игровая команда







[wake()] «–» проснуться, пробудиться «–» Выводит значение game.wake wake Игровая команда







walk(arg1, arg2) arg1 (объект): undefined; arg2 (строка): направление движения (n, s, w, e, u, d, ne, se, sw, nw) [север, c], [юг, ю], [запад, з], [восток, в], [вверх, вв, наверх, подняться], [вниз, вн, спуститься, опуститься], [северо-восток, с-в, св], [северо-запад, с-з, сз], [юго-восток, ю-в, юв], [юго-запад, ю-з, юз] Numpad 8, Numpad 2, Numpad 4, Numpad6, Numpad ., Numpad 0, Numpad 9, Numpad 3, Numpad 1, Numpad 7 1. Если свойство arg2 отсутствует у текущей локации выводит значение game.noWayMsg и завершает выполнение.
2. Создает массив, содержащий объекты со свойством door равным true.
3. В массиве полученном в п.2 ищется объект, элементами свойства loc которого являются значения player.loc и свойства arg2 текущей локации.
4. Если такой объект (см. п.3) найден и у него есть свойство closed, равное true выводит сообщение «Путь прегражден <...>» и завершает выполнение.
5. Вызывает функцию move(player, <значение свойства arg2 текущей локации>).
walk / в качестве названия свойств объекта gameCommands используются: north, south, west, east, up, down, northEast, southEast, southWest, northWest Игровая команда







wear(arg1) arg1 (объект): объект для действия надеть «–» 1. Если у arg1 нет свойства worn выводит сообщение «<arg1> нельзя надеть.» и завершает выполнение.
2. Если свойство worn arg1 равно true выводит сообщение «<arg1> уже надет.» и завершает выполнение.
3. Если свойство loc arg1 не равно 'player' вызывает функцию take(arg1).
4. Если свойство loc arg1 равно 'player' устанавливает значение свойства worn arg1 равным true и выводит сообщение «Вы надеваете <arg1>.»
Если вы пишите свой метод wear рекомендую прочитать про особенности свойства worn в Приложении 2 «Стандартные свойства объектов».
wear Игровая команда

Приложение 2

Стандартные свойства объектов

Имя свойства Тип значения Класс объектов, в котором может присутствовать свойство Описание Значение по-умолчанию
about Строка game Сообщение, выводимое при вызове функции about. protoparser.js
Версия: 8
protoparser.js is copyright (c) 2018-2020, 2022 Alexey Galkin, released under the MIT license.





advice Строка game Сообщение, выводимое при вызове функции advice. В этой игре не предусмотрено подсказок.





ageRating Строка game Возрастные ограничения игры. undefined





altAKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + A». совет





altCKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + C». очистка





altHKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + H». история





altJKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + J». лог





altMKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + M». локации





altSlashKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + ?». помощь





altVKey Строка game Команда, передаваемая интерпретатору при нажатии на комбинацию клавиш «Alt + V». версия





author Строка game Автор(ы) игры. undefined





cancelRestartMsg Строка game Сообщение, которое выводится, если пользователь ввел отличный от «д» символ при выводе подтверждения на перезагрузку. Команда отменена.





closed Логическое значение thing Наличие свойства является признаком того, что объект можно открыть и закрыть. Если значение равно true объект закрыт, false – открыт. undefined





commandHistory Массив строк game Массив сохраненных команд, введенных пользователем. «» (пустое значение)





commandHistoryIndex Число game Индекс выбранной команды в истории команд. -1





commandHistoryIsEmptyMsg Строка game Сообщение, выводимое при вызове функции repeat, если массив game.commandHistory пуст. История команд пуста.





commandHistoryLength Число game Максимальное число хранимых в game.commandHistory команд. 10





commandHistoryOffMsg Строка game Сообщение, выводимое при вызове функции history в случае, когда game.commandLength равно нулю. В этой игре не ведется история команд.





commandTemplate Строка game Содержимое строки ввода. «» (пустое значение)





comment Строка game Идентификатор комментария (игнорируется парсером). .





confirmRestartMsg Строка game Сообщение, выводимое при вызове функции restart. Вы действительно хотите начать игру заново?





d Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении вниз. undefined





desc Строка player, room, thing Описание объекта, выводимое по команде ОСМОТРЕТЬ [ПРЕДМЕТ]. undefined





door Логическое значение thing Значение true данного свойства указывает на то что объект ведет себя как дверь (если закрыта - не позволяет выйти из локации). Для нормальной работы объекта-двери, кроме свойства door, необходимо наличие у объекта свойства closed (с любым значением), а также свойства loc, элементами которого являются имена локаций в которых присутствует данный объект-дверь.
Свойство loc объекта-двери может содержать и один элемент (см. главу «Виртуальные» двери и «мнимые» выходы»).
undefined





downloadLogMsg Строка game Текст ссылки для скачивания лога. Скачать лог игры





e Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на восток. undefined





edible Логическое значение thing Если значение свойства равно true объект может быть «съеден» (удаляется из игры). undefined





emptyCommandMsg Строка game Сообщение, которое выводится, если введена пустая строка. Простите?





examined Число thing Количество раз которое был осмотрен предмет. Предмет, который ни разу не был осмотрен, не имеет свойства examined (undefined). undefined





gend Строка player, thing Род или число имени объекта. Может принимать следующие значения: m – мужской род, f – женский род, n – средний род, p – множественное число. undefined





head Строка room Название локации. undefined





help Строка game Игровая справка. Для ввода команд используйте шаблон ГЛАГОЛ [СУЩЕСТВИТЕЛЬНОЕ]. Регистр и лишние пробелы не учитываются. Команда должна состоять не более чем из двух слов. Полный список стандартных команд, поддерживаемых протопарсером, указан в Приложении 1.

Системные команды:
  • Сохранить <имя> – сохраняет текущее состояние игры под заданным именем;
  • Загрузить <имя> – загружает сохраненное состояние игры с заданным именем;
  • Заново – перезапускает игру;
  • Повторить (Numpad +) – выполняет последнюю команду, сохраненную в истории команд;
  • История (Alt + H) – выводит список команд, сохраненных в истории команд;
  • Счет (Numpad *) – выводит текущий счет и количество сделанных ходов с начала игры;
  • Версия (Alt + V) – выводит информацию о версии protoparser.js и лицензиях;
  • Очистка (Alt + C) – удаляет с экрана игровой текст;
  • Совет (Alt + A) – выводит советы по игре (если автор их написал);
  • Стрелка вверх – добавляет команду из истории команд в строку ввода: от последней к более ранним;
  • Стрелка вниз – добавляет команду из истории команд в строку ввода: от самой первой к более поздним;
  • Лог (Alt + J) – выводит ссылку для скачивания лога;
  • Локации (Alt + M) – выводит список посещенных локаций и находившихся в них объектов, которые были осмотрены;
  • Справка (Alt + ?) – выводит краткую справочную информацию по управлению игрой.





hidden Логическое значение player, thing Если значение свойства равно true, при выводе описания локации, не выводится значение свойства sceneDesc объекта (если оно есть). Кроме того, в описание локации название объекта не отображается в перечне объектов, которые в ней находятся. undefined





hiddenPossession Логическое значение thing При установленном значении true имя объекта не выводится при вызове функции inventory. undefined





ifid Строка game IFID игры. undefined





info Строка game Информация об игре. undefined





jump Строка game Сообщение, выводимое при вызове функции jump. Вы подпрыгиваете.





kiss Строка game Сообщение, выводимое при вызове функции kiss. Вы сдерживаете свой порыв.





license Строка game Лицензия (условия распространения) игры. undefined





listen Строка game Сообщение, выводимое при вызове функции listen. Вы не слышите ничего необычного.





loadedMsg Строка game Сообщение, выводимое при вызове функции loadGame в случае успешной загрузки сохраненной игры (функция load вернула true). Игра загружена.





loadForbiddenMsg Строка game Сообщение, выводимое при вызове функции loadGame, если значение game.noSaveLoad равно true. В этой игре восстановление запрещено.





loc Строка, массив строк player, thing Расположение объекта на начало игры либо его текущее положение. Объект может одновременно находиться в нескольких локациях. Свойство player.loc может содержать только одно значение: имя текущей (начальной) локации. undefined





locked Логическое значение thing Наличие свойства указывает на то, что объект может быть заперт и отперт. Значение true указывает на то, что объект заперт, false - на то, что не заперт. undefined





longCommandMsg Строка game Сообщение, выводимое, если команда состоит более чем из двух слов. Команда должна состоять не более чем из двух слов.





lostMsg Строка game Сообщение, выводимое при вызове функции end с аргументом 0. *** Вы проиграли! ***





maxCarried Число player Максимальное число предметов, которое может находиться в инвентаре, включая надетые предметы. undefined





maxScore число game Максимальный счет, который игрок может набрать. Протопарсер не проверяет является ли значение maxScore действительно максимальным. undefined





moved Число player, thing Количество раз, которое предмет менял местоположение (значение собственного свойства loc). Учитываются только перемещения, выполненные с помощью функции move. undefined





n Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на север. undefined





nam Массив строк player, thing Имена объекта (синонимы). Первые шесть значений - склонения объекта по падежам (и.п., р.п., д.п. в.п., т.п., п.п.). Эти значения используются при выводе имени объекта. За ними могут следовать произвольное количество имен-синонимов. undefined





ne Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на северо-восток. undefined





noSaveLoad Логическое значение game При установленном значении true сохранение и восстановление игры невозможно. undefined





noScore Логическое значение game При установленном значении true функции reward и score недоступны (движок продолжает считать ходы и это значение доступно в game.turn). undefined





noScoreMsg Строка game Сообщение, выводимое при вызове функции score, если значение game.noScore равно true. В этой игре не ведется счет.





noThingMsg Строка game Сообщение, выводимое, если предмета для действия нет ни в текущей локации, ни в инвентаре. Здесь нет этого предмета.





notLoadedMsg Строка game Сообщение, выводимое при вызове функции loadGame в случае, если не удалось загрузить сохраненную игру (функция load вернула false). Ошибка загрузки.





notSavedMsg Строка game Сообщение, выводимое при вызове функции saveGame в случае, если не удалось сохранить игру (функция save вернула false). Ошибка сохранения.





noVisitedMsg Строка game Сообщение, об отсутствии посещенных локаций. Вы пока не посетили ни одной локации.





noWayMsg Строка game Сообщение, выводимое при попытке двигаться в направлении для которого отсутствует выход из данной локации. В этом направлении нельзя пойти.





num0Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «0» цифровой клавиатуры. вниз





num1Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «1» цифровой клавиатуры. юго-запад





num2Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «2» цифровой клавиатуры. юг





num3Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «3» цифровой клавиатуры. юго-восток





num4Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «4» цифровой клавиатуры. запад





num5Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «5» цифровой клавиатуры. осмотреться





num6Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «6» цифровой клавиатуры. восток





num7Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «7» цифровой клавиатуры. северо-запад





num8Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «8» цифровой клавиатуры. север





num9Key Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «9» цифровой клавиатуры. северо-восток





numAddKey Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «Numpad +». повторить





numDecimalPointKey Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «.» цифровой клавиатуры. вверх





numDivideKey Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «/» цифровой клавиатуры. инвентарь





numMultiplyKey Строка game Команда, передаваемая интерпретатору при нажатии на клавишу «*» цифровой клавиатуры. счет





nw Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на северо-запад. undefined





overburdenMsg Строка game Сообщение, выводимое при попытке переместить предмет в инвентарь, если число предметов в нем равно player.maxCarried. Вы несете слишком много вещей.





placedHereMsg Строка game Сообщение о том, что в локации присутствуют предметы за которым следует перечисление соответствующих предметов (генерируется движком автоматически). Здесь есть





playerHasMsg Строка game Сообщение о том, что в инвентаре игрока присутствуют предметы за которым следует перечисление соответствующих предметов (генерируется движком автоматически). У вас с собой:





playerHasNothingMsg Строка game Сообщение о том, что в инвентаре игрока нет предметов. У вас с собой ничего нет.





points Число game Число очков, набранных игроком. 0





prompt Строка game Символьное представление приглашения командной строки. >





s Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на юг. undefined





savedMsg Строка game Сообщение, выводимое при вызове функции saveGame в случае успешного сохранения игры (функция save вернула true). Игра сохранена.





saveForbiddenMsg Строка game Сообщение, выводимое при вызове функции saveGame, если значение game.noSaveLoad равно true. В этой игре сохранение запрещено.





se Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на юго-восток. undefined





sing Строка game Сообщение, выводимое при вызове функции sing. Вы запеваете подходящую случаю песню.





sleep Строка game Сообщение, выводимое при вызове функции sleep. Сейчас не время для этого.





smell Строка game Сообщение, выводимое при вызове функции smell. Вы не чувствуете ничего необычного.





spec Строка room, thing, events, globalVerbs, game, player Тип объекта, свойством которого является. Может принимать значения: game, player, globalVerbs, events, room, thing. Данное свойство необходимо добавлять, только если объект относится к классу room или thing. В остальных случаях протопарсер автоматически добавит стандартному объекту свойство spec, инициализировав его в соответствии с идентификатором объекта. game, player, globalVerbs, events





stopped Логическое значение game При установленном значении true командная строка становится неактивной. false





sw Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на юго-запад. undefined





takeable Логическое значение thing Если значение свойства равно true объект может быть взят (добавлен в инвентарь). undefined





tests Логическое значение game Если значение свойства равно true при старте игры запустится автотестирование. undefined





text Строка thing Наличие свойства указывает на то, что предмет может быть прочитан. Значением свойства является строка – результат команды ЧИТАТЬ. undefined





think Строка game Сообщение, выводимое при вызове функции think. Вы все время думаете.





title Строка game Название игры. undefined





turn Число game Число сделанных игроком ходов (введенных пользователем команд) с начала игры. Значение свойства не увеличивается, если были вызваны системные функции (метакоманды) или если пользователь ввел команду некорректно или не полностью. 0





u Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении вверх. undefined





unknownCommandMsg Строка game Сообщение, выводимое, если команда не содержит глагол, либо он не известен программе. Команда непонятна.





version Строка game Версия игры. undefined





visits Число room Число посещений локации персонажем игрока. Ни разу не посещенная локация не имеет свойства visits (undefined). Стартовая локация (значение player.loc) имеет начальное значение свойства «1». undefined / 1





w Строка room Имя объекта типа room, в который можно попасть из текущей локации при движении на запад. undefined





wait Строка game Сообщение, выводимое при вызове функции wait. Проходит немного времени.





wake Строка game Сообщение, выводимое при вызове функции wake. Это не сон.





winMsg Строка game Сообщение, выводимое при вызове функции end с аргументом 1. *** Вы победили! ***





worn Логическое значение thing Наличие свойства указывает на то, что предмет может быть надет. Если значение свойства равно true объект считается надетым. При использовании команды ИНВЕНТАРЬ информация о том, что объект надет будет отображена рядом с именем объекта. Данное свойство является простым флагом, указывающим надет ли объект или нет, однако, «физически» только свойство loc объекта определяет его местонахождение, поэтому возможны (хотя и нежелательны) ситуации, когда объект «надет», но поскольку его свойство loc не равно player, то «надетый» предмет становится недоступным для любых действий с ним. Стандартная функция-обработчик wear автоматически присваивает loc надеваемого объекта значение player. Однако, если вы пишите свой метод wear вы должны будете самостоятельно делать эту операцию. Аналогично, если вы изымаете куда-то объект из инвентаря проверяйте, что worn объекта не равно true. undefined





year Строка game Год публикации игры. undefined

Приложение 3

Исходный код игры «Фантазия»

Ниже приводится полный код игры «Фантазия», которую мы создавали на протяжении этого руководства в качестве обучающего примера. Код содержится в файле story.js в папке examples/Fantasia.

var game = {
	title: 'Фантазия',
	author: 'Алексей Галкин &lt;johnbrown&gt;',
	year: 2018,
	license: 'MIT',
	version: '8.0',
	info: 'Небольшая демонстрационная игра на движке protoparser.js.',
	notSavedMsg: 'Ошибка сохранения. Игра не сохранена.',
	tests: false,
	maxScore: 5
}

var player = {
	nam: ['вы', 'себя', 'себе', 'себя', 'собой', 'себе'],
	desc: 'Вы - обычный турист, неизвестно как попавший сюда.',
	hidden: true,
	loc: 'hall'
}

var globalVerbs = {
    sleep: function() {
        p('Едва ли вам теперь удастся заснуть.')
        return true
    }
}

var events = {
	init: function() {
		p('Вы стоите в длинном широком коридоре какого-то старого особняка или даже замка, не имея ни малейшего понятия как здесь очутились, и, что еще важнее, как отсюда выбраться.')
		gameCommands.take[1].push('схватить', 'завладеть')
		gameCommands.sneeze = ['sneeze', ['чихать', 'чихнуть'], 0]
	},
	beforeAll: function(com, obj, optional) {
		if ((com == 'take' || com == 'wear') && obj == hat && !hat.moved) {
			p('Едва вы успели коснутся шляпы, внезапный порыв ветра подхватил ее, и понес в сторону озера. С удивлением вы обнаруживаете яблоко на том месте, где только что лежала шляпа.')
			move(hat, 'lake')
			apple.loc = 'tower'
			return true
		}
		if (player.loc == 'tower' && optional == 'n' && tower.n == 'tree') {
			p('Вы осторожно перебираетесь на верхушку дерева.')
			walk(undefined, 'n')
			return true
		}
		if ((player.loc == 'glade' && optional == 'n' && coin.loc == 'oldman') || (player.loc == 'lake' && optional == 's')) p('Вы усиленно нажимаете на весла, и в несколько мощных гребков преодолеваете половину озера.')
	},
	afterAll: function(com, obj, optional) {
		watch.worn ? game.prompt = new Date().toLocaleTimeString() + ' > ' : game.prompt = '> '
		if (com == 'show' && player.loc == 'glade') {
			if (obj == coin) {
				p('Глаза старика радостно загораются. «Вот спасибо-то! Будет старику прибавка к пенсии. Забирай лодку, коль не передумал.»')
				move(coin, 'oldman')
			} else p('Старик с видом знатока кивает: «' + capitalize(obj.nam[0]) + ' что надо!»')
		}
		if (com == 'say' && optional == 'домой' && hat.worn) {
			p('Едва вы произнесли это слово, как, внезапно, обнаружили, что сидите уставившись в экран. Ха-ха! Это была всего лишь игра. Спасибо, что уделили ей время. Приходите еще!')
 			end(1)
		}
		if (player.loc == 'hall' || player.loc == 'round') {
			document.body.style.color = 'white'
			document.body.style.backgroundColor = 'black'
            document.getElementById('input').style.color = 'white'
		} else {
			document.body.style.color = 'black'
			document.body.style.backgroundColor = 'white'
            document.getElementById('input').style.color = 'black'
		}
		player.loc == 'glade' && oldman.activity()
		if (oldman.loc == player.loc && com == 'say') oldman.response(optional)
	}
}

var ticket = {
	nam: ['билет', 'билета', 'билету', 'билет', 'билетом', 'билете', 'прямоугольник', 'текст'],
	spec: 'thing',
	desc: 'Картонный прямоугольник голубого цвета, на котором изящными золотистыми буквами напечатан какой-то текст.',
	text: '<div style="color:yellow; background-color: blue;"><p style="text-align: center;font-weight: bold;">«Уважаемый посетитель # 3677!</p><p style="font-style:italic;">Мы рады приветствовать вас в Фантазии. Пожалуйста, ни чему не удивляйтесь и чувствуйте себя как дома. Но, если вам и впрямь надо вернуться домой используйте шляпу».</p></div>',
	loc: 'round',
	takeable: true,
	drop: function() {
		p('Билет еще пригодится.')
		return true
	}
}

var apple = {
	spec: 'thing',
	nam: ['яблоко', 'яблока', 'яблоку', 'яблоко', 'яблоком', 'яблоке', 'фрукт', 'плод'],
	desc: 'Ярко-красный плод так и манит его съесть.',
	takeable: true, edible: true,
	loc: '',
	eat: function() {
		remove(this);
		move(seed, 'player')
		p('Яблоко было таким вкусным, что вы даже не заметили, как оно исчезло у вас во рту, а вместо него в руках оказалась косточка.')
		return true
	} 
}

var seed = {
	spec: 'thing',
	nam: ['косточка', 'косточки', 'косточке', 'косточку', 'косточкой', 'косточке', 'кость', 'семя', 'семечко', 'зернышко', 'зерно'],
	desc: 'Крохотное золотистое зернышко чуть подрагивает словно живое. Кажется, ей не очень здесь нравится. Наверное, она хочет туда, где ей будет хорошо.',
    sceneDesc: 'Золотистое зернышко крутится и подпрыгивает на каменном полу.',
	drop: function() {
		if (player.loc == 'tower') {
			p('Едва косточка коснулась земли, как стены башни и пол затряслись, послышался громкий треск сучьев, словно какой-то великан прокладывал себе дорогу через чащу леса. В действительности же, дорогу себе прокладывало огромное дерево, непонятно как за какие-нибудь пять минут достигшее своей верхушкой вершины башни.')
			remove(this)
			tower.desc = 'Открытая площадка башни позволяет видеть округу на много километров. Озеро на севере заграждают кроны огромного дерева, вплотную примкнувшие к башне. Вниз ведет лестница.'
			tower.n = 'tree'
			objTree.loc = ['tower', 'tree', 'glade']
			return true
		}
	},
	loc: '',
	takeable: true
}

var hall = {
	spec: 'room',
	head: 'Длинный коридор',
	desc: 'Серые каменные стены коридора, кажется, покрыты пылью многих веков. На севере расположена невысокая дверь. Каменная спиральная лестница поднимается высоко вверх. Возле стены, стоит полный рыцарский доспех с опущенным забралом. Вас не покидает чувство, что за вами наблюдают.',
	n: 'round',
	u: 'tower',
	say: function(obj, word) {
		say(undefined, word);
		p('Эхо коридора чужим голосом повторило «' + capitalize(word) + ', ' + word + ', ' + word + '...».')
		return true
	},
	walk: function(obj, dir) {
		if (dir == 'u' && !tower.visits) p('После полумрака коридора яркий дневной свет на несколько секунд ослепляет вас. Открывшаяся вашему взору картина завораживает.')
	}
}

var stairs = {
	spec: 'thing',
	nam: ['лестница', 'лестницы', 'лестнице', 'лестницу', 'лестницой', 'лестнице', 'ступеньку', 'ступень'],
	desc: 'Узкая винтовая лестница, на которой с трудом разминутся даже два человека, соединяет башню и коридор.',
	hidden: true,
	loc: ['hall', 'tower']
}

var wall = {
	spec: 'thing',
	nam: ['стена','стены','стене','стену','стеной','стене', 'пыль', 'грязь', 'камень', 'камни'],
	desc: 'Стена сложена из огромных серых камней, первоначальный цвет которых не представляется возможным определить из-за толстого слоя многовековой пыли и грязи, которыми они покрыты.',
	hidden: true,
	loc: ['hall', 'round']
}

var armour = {
	spec: 'thing',
	nam: ['доспех', 'доспеха', 'доспеху', 'доспех', 'доспехом','доспехе', 'доспехи', 'рыцаря', 'забрало', 'шлем'],
	desc: 'Потемневший от времени доспех даже сейчас производит угрожающее впечатление. Доспех несомненно пуст, и все-же вам не хотелось бы оставаться с ним наедине.',
	hidden: true,
	loc: 'hall',
	closed: true,
	take: function() {
		p('Доспех слишком тяжел для вас.')
		return true
	},
	wear: function() {
		p('Глупо таскать такую кучу металла на себе.')
		return true
	},
	attack: function() {
		if (key.loc == 'armour') p('Внутри доспеха что-то звякнуло.')
		else p('Доспех отозвался глухим звуком удара.')
		return true
	},
	open: function() {
		if (key.loc == 'armour') {
			p('Едва вы подняли  забрало, что-то блестящее выскочило из шлема и со звоном упало на каменный пол.')
			move(key, 'hall')	
		} else if (armour.closed) 
			p('Вы поднимите забрало и заглядываете внутрь, но там только паутина.')
		else p('Забрало уже поднято.')
		armour.closed = false
		hall.desc = hall.desc.replace('опущенным', 'поднятым')
		return true
	},
	close: function() {
		if (armour.closed) p('Забрало уже опущено.')
		else p('Вы опускаете забрало.')
		hall.desc = hall.desc.replace('поднятым', 'опущенным')
		armour.closed = true
		return true
	}
}

var round = {
	spec: 'room',
	head: 'Круглая комната',
	desc: 'Большая круглая комната выглядит пустой и необжитой. Кажется, хозяин (или, быть может, архитектор) поленился придать ей индивидуальность. Единственное украшение комнаты, не считая вас, - деревянная дверь на юге.',
	s: 'hall'
}

var heavy_door = {
	spec: 'thing',
	nam: ['дверь', 'двери', 'двери', 'дверь', 'дверью', 'двери'],
	desc: 'Тяжелая дубовая дверь, кажется, помнит еще те времена, когда слово «рыцарь» было не просто красивым эпитетом.',
	hidden: true,
	loc: ['round','hall'],
	closed: true,
	locked: true,
	door: true,
	unlock: function() {
		if (heavy_door.locked && heavy_door.closed) {
			if (key.loc == 'player') p('Вы вставляете ключ в замочную скважину и с большим трудом проворачиваете его.')
			else {
				p('Без ключа дверь не открыть.')
				return true
			}
		}
	},
	lock: function() {
		if (!heavy_door.locked && heavy_door.closed) {
			if (key.loc == 'player') p('Вы вставляете ключ в замочную скважину и с большим трудом проворачиваете его, придавливая рукой тяжелую дверь.')
			else {
				p('Без ключа дверь не запереть.')
				return true
			}
		}
	}
}

var key = {
	spec: 'thing',
	nam: ['ключ', 'ключа', 'ключу', 'ключ', 'ключом', 'ключе'],
	desc: 'Тяжелый бронзовый ключ.',
	loc: 'armour',
	takeable: true,
	drop: function() {
		if (player.loc == 'lake') {
			remove(this)
			p('Бульк!<br>Ключ быстро исчезает в мутной воде.')
			return true
		}
	}
}

var glade = {
	spec: 'room',
	head: 'Поляна',
	desc: 'Небольшая поляна залита солнечным светом. В центре поляны высится огромное дерево. На севере поблескивает на солнце небольшое озеро, прохладный ветерок с которого вас приятно освежает. На берегу, рядом с водоемом, ссутулившись, сидит, глядя на воду старик. В метре от него к берегу причалена старая лодка. Вдалеке, на северо-востоке виднеется выход из парка.',
	u: 'tree',
	ne: 'entrance',
	n: 'lake',
	walk: function(obj, dir) {
		if (dir == 'n' && coin.loc != 'oldman') {
			p('«А ну отойди от лодки! Ишь, ты, хулиган!». Резкий окрик старика удержал вас от попытки воспользоваться лодкой.')
			return true
		}
	}
}

var lake = {
	spec: 'room',
	head: 'Озеро',
	desc: 'В круглом голубом озере отражается небо. Поляна расстилается далеко на юге.',
	s: 'glade',
	walk: function(obj, dir) {
		if (dir != 's' && dir != 'u') {
			if (dir == 'd') p('У вас нет акваланга.')
			else p('Сейчас не время кататься по озеру.')
			return true
		}
	}
}

var entrance = {
	spec: 'room',
	head: 'Вход в парк',
	desc: 'Широкие решетчатые ворота на севере отделяют «Фантазию» от внешнего мира. У входа в парк стоит автомат по продаже билетов. На юго-западе раскинулась широкая поляна.',
	sw: 'glade',
	n: ''
}

var gate = {
	spec: 'thing',
	nam: ['ворота', 'ворот', 'воротам', 'ворота', 'воротами', 'воротах', 'фигуры', 'фигуру', 'животных', 'животное', 'створки', 'створка', 'дверь', 'двери'],
	gend: 'p',
	desc: 'Чугунные прутья ворот украшены цветным фигурами сказочных животных.',
	hidden: true,
	loc: 'entrance',
	door: true,
	locked: true,
	closed: true,
	unlock: function() {
		p('Вы внимательно осматриваете ворота, но ни замка, ни задвижки, ни другого запирающего устройства не находите. Потратив еще несколько минут изучая створки и пытаясь найти хоть какие-нибудь лазейки, вы пришли к выводу, что ворота явно волшебные, и просто так не откроются.')
		return true
	}
}

var redBox = {
	spec: 'thing',
	nam: ['автомат', 'автомата', 'автомату', 'автомат', 'автоматом', 'автомате', 'ящик', 'щель', 'лоток', 'отверстие'],
	desc: 'Высокий с человеческий рост красный ящик чем-то неуловимо напоминает первые модели игровых автоматов. Рядом с щелью для купюр имеется отверстие откуда выходят билеты, а ниже под ним - лоток для сдачи.',
	hidden: true,
	loc: 'entrance',
	attack: function(obj, optional, verb) {
		if ((verb == 'ударить' || verb == 'пнуть' || verb == 'стукнуть') && !redBox.beaten) {
			p('Вы со всей силой ударяете по автомату, который тут же, в отместку, выплевывает в вас серебряную монету.')
			move(coin, 'entrance')
			redBox.beaten = true
			reward(5)
			return true
		}
	},
	beaten: false
}

var coin = {
	spec: 'thing',
	nam: ['монета', ' монеты', 'монете', 'монету', ' монетой', 'монете', 'деньги'],
	desc: 'Маленькая серебряная монетка с надписью «Жетон Банка Фантазии».',
	loc: '',
	takeable: true
}

var boat = {
	spec: 'thing',
	nam: ['лодка', 'лодки', 'лодке', 'лодку', 'лодкой', 'лодке', 'краску'],
	desc: 'Старое корыто, коим, по-сути, и была лодка неизвестно каким чудом держалась на воде. Краска на ее бортах давно потрескалась и отлетела. Внутри места хватало ровно на одного человека, да еще на бутылку в которую можно положить послание «Я был так глуп, что решил отправиться в плавание на этой скорлупе. Не повторяйте моей ошибки».',
	hidden: true,
	loc: ['glade', 'lake']
}

var tower = {
	spec: 'room',
	head: 'Башня',
	desc: 'Открытая площадка башни позволяет видеть округу на много километров. В отдалении на севере блестит небольшое озеро и виднеется выход из парка. Вниз ведет лестница.',
	d: 'hall',
	walk: function(obj, dir) {
		if (dir != 'u' && dir != 'd') { p('Слишком высоко. Надо найти какой-то другой способ спуститься вниз.')
		return true
		}
	},
	jump: function() {
		p('С такой высоты? Ну, уж нет!')
		return true
	}
}		

var hat = {
	spec: 'thing',
	nam: ['шляпа', 'шляпы', 'шляпе', 'шляпу', 'шляпой', 'шляпе', 'надпись', 'ленту'],
	desc: 'Серая широкополая шляпа выглядит весьма помятой. Сбоку к шляпе приколота какая-то лента с надписью.',
	read: function() {
		if (hat.loc == 'player' && !hat.worn) p('Надпись на ленте гласила: «Надень меня, скажи «Домой!», и дом увидишь свой родной».')
		else p('Вам придется взять в руки шляпу, чтобы прочитать текст на ленте.')
		return true
	},
	loc: 'tower',
	takeable: true, worn: false
}

var tree = {
	spec: 'room',
	head: 'Вершина дерева',
	desc: 'Широкие кроны дерева позволяют вам свободно, и, даже, с некоторым удобством разместиться на верхушке. Прямо под вами далеко внизу раскинулась залитая солнцем поляна, а в нескольких метрах с южной стороны высится еще одна громада - старая башня.',
	s: 'tower',
	d: 'glade'
}

var objTree = {
	spec: 'thing',
	nam: ['дерево', 'дерева', 'дереву', 'дерево', 'деревом', ' дереве', 'крону', 'лист', 'листья'],
	desc: 'Эта громада производит впечатление спящего великана, тяжело покачиваясь и шумя листьями при сильных порывах ветра. Ее широкие кроны опираются на башню, доставая до самой ее вершины.',
	hidden: true,
	loc: ''
}

var oldman = {
	spec: 'thing',
	nam: ['старик', ' старика', 'старику', 'старика', 'стариком', 'старике', 'лодочника', 'шорты', 'телогрейку', 'старичка', 'деда'],
	desc: 'Невысокий лысенький старичок в поношенной телогрейке и в цветастых молодежных шортах, неизвестно как на нем очутившихся, поглощен разглядыванием озера. Кажется, ничто на свете его не трогает.',
	hidden: true,
	loc: 'glade',
	activity: function() {
		var activityNumber = Math.round(Math.random() * 9)
		if (activityNumber < 5) {
			var activityText = [
				'Старик украдкой поглядывает на вас.',
				'Старик что-то бормочет себе под нос.',
				'Старый лодочник в задумчивости чешет макушку.',
				'Старик бормочет что-то о вездесущих туристах.',
				'Старец громко похрапывает.'
			]
			p(activityText[activityNumber])
		}
	},
	response: function(optional) {
		switch(optional) {
			case 'здравствуйте':
			case 'здравствуй':
			case 'привет': var response = 'И тебе не хворать.'
			break
			case 'фантазия':
			case 'парк':
			case 'замок':
			case 'башня': var response = 'Парк-то здешний вроде заповедника волшебного - разными чудесами полнится. Видал дерево? А с утра не было. Небось твоя работа? Меня-то, люди добрые Михеем кличут, я здесь, вроде как, сторож, приглядываю за всем, порядок блюду, в смысле, блюжу, охраняю, короче.'
			break
			case 'михей': var response = 'Шо?'
			break
			case 'озеро': var response = 'Давно тут сижу. Озеро широкое, озеро глыбокое - вплавь не переплыть, с ахвалангом не перейтить. Вот только лодчонка моя могеть озеро тутошнее того... одолеть.'
			break
			case 'шляпа':
			case 'шляпу':
			var response = 'Шляпу-то? Видал-видал. Пролетела тут, давеча, как фанера над Парижем, да и в воду бултыхнулась.'
			break
			case 'монета':
			case 'монету':
			case 'деньги':
			case 'лодка':
			case 'лодку': if (coin.loc == 'oldman') var response = 'Ступай милый, ступай!'
				else if (coin.loc == 'player') {
				var response = 'Вот спасибо-то! Будет старику прибавка к пенсии. Забирай лодку, коль не передумал.'
				move(coin, 'oldman')
				}
				else var response = 'Помоги дедушке материально, и катайся себе на здоровье, милок!'
				break
			default: var response = 'Чего говОришь-то?'
		}
		p('Старик отвечает: «' + response + '»')
	}
}

var watch = {
	spec: 'thing',
	nam: ['часы', 'часов', 'часам', 'часы', 'часами', 'часах', 'пластик', 'ремешок'],
	desc: 'Легендарные электронные часы «Motnana» из благородного черного пластика с элегантным гибким ремешком для удобного ношения. Вечная классика.',
	loc: 'player',
	takeable: true, worn: false,
	drop: function() {
		if (player.loc == 'lake') {
			p('Пустить на дно такие часы? Ну уж нет!')
			return true
		}
	}
}

function sneeze() {
	p('– Апч-хи!!!')
}

Примечания


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

  2. Объект localStorage в вашем браузере должен быть доступен для записи и чтения.

  3. Вообще говоря, метод afterAll может сработать и после beforeAll, если последний вернет true.

  4. Если введеная команда не может быть передана функции-обработчику, интерпретатор выдаст сообщение об этом, а методы events не будут вызваны (см. Главу 9 «Обработка команд»).

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

  6. Аргументы в квадратных скобках являются необязательными; функция в квадратных скобках является «виртуальной»

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