Protoparser.js – легковесный веб-движок (библиотека) для интерактивных текстовых игр с вводом команд («парсеров»).
Приставка «прото» в названии движка, в переводе с древнегреческого, означает «первый». Интерфейс командной строки протопарсера оперирует всего двумя понятиями: ГЛАГОЛ и СУЩЕСТВИТЕЛЬНОЕ. В этом он похож на первые текстовые игры-приключения. Вот основные особенности и возможности протопарсера:
Для написания игры вам понадобится знание основ JavaScript'а. Предполагается, что читатель, в той или иной степени, знаком с этим языком, поэтому некоторые моменты, связанные с сугубо языковыми особенностями js могут быть пропущены. Однако, в связи с широкой распространенностью языка, не представляет сложности самостоятельно найти в интернете необходимые материалы, рассчитанные на ваш уровень подготовки.
Работа над протопарсером началась 17 февраля 2018, а пару месяцев спустя вышла первая версия движка – 1(81). Это руководство описывает версию 8.
В этом руководстве использованы следующие обозначения:
Моноширным шрифтом
выделены фрагменты кода, имена переменных, файлов, и т.п.Создавая свою игру, вам предстоит иметь дело с объектами. Объект, говоря по-простому, – это набор пар «свойство-значение». Таких пар у объекта может быть сколь угодно много. В основном, вы будете использовать стандартные свойства объектов. Но, кроме того, вы можете добавлять и свои свойства. Вообще, протопарсер работает с 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
Однако следует знать и помнить, что в файл сохранения попадают только переменные, которые являются свойствами стандартных объектов.
Прежде чем мы начнем подробно рассматривать каждый класс объектов необходимо рассказать о свойстве spec
. Все объекты, обладающие данным свойством, считаются «стандартными». Объектам game
, player
, lobalVerbs
, events
движок автоматически добавляет свойство spec
при загрузке игры. Это означает, что вам не нужно добавлять его в свой код. Однако! Если ваш объект является комнатой (локацией) или предметом вы должны добавить ему свойство spec
. Если наш объект – предмет, мы указываем spec: 'thing'
, если комната (локация) – spec: 'room'
.
Одним из главных преимуществ протопарсера является то, что для написания игры вам не обязательно иметь доступ к какой-то определенной ОС, устанавливать дополнительные программы. Вам, также, не стоит беспокоиться насчет IDE. Достаточно иметь на своем устройстве браузер с поддержкой JavaScript и любую программу, в которой можно набрать и сохранить текст.
В папке protoparser
уже лежит готовый шаблон для вашей первой игры. Он называется story.js
. Ваша игра может иметь какое-то другое название, но имя файла story.js
не должно меняться. В этом руководстве мы, шаг за шагом, будем создавать нашу первую игру. В конце руководства в Приложении 3 есть полный исходный код этой игры, а в папке examples/Fantasia
уже лежит готовый файл c игрой story.js
. Вы можете запустить файл index.html
, чтобы посмотреть, как выглядит готовая игра.
Во время написания игры удобно держать текстовый редактор и браузер открытыми, время от времени переключаясь между ними. После того, как вы сохраните изменения в редакторе просто обновите окно с игрой в браузере, чтобы проверить как все работает. Если ваш текстовый редактор пишет, что не может открыть файл, выберите «Открыть как текст».
Возможно, вы захотите писать игру прямо в браузере, перейдя в т.н. «режим разработчика». Работать в нем не сложно, но из-за обилия вкладок, поначалу, может быть непривычно. Вы можете писать игры в привычном для вас редакторе, а переходить в «режим разработчика» только для отладки. О работе в этом режиме будет рассказано в главах Сохранение и загрузка и Тестирование и отладка.
В протопарсере настройки игры представляют собой объект game
и располагаются в файле story.js
, как и любые другие игровые объекты. Отсутствие данного объекта в story.js
приведет к ошибке.
Создадим объект game:
var game = {}
Театр, как известно, начинается с вешалки, а игра с названия. Давайте назовем нашу игру «Фантазия»:
title: 'Фантазия'
Теперь, добавим автора (не забудьте поставить свое имя):
author: 'Алексей Галкин <johnbrown>'
Поскольку тэги (символы <
и >
) никак не отображаются в окне браузера мы заменили их специальными символами <
и >
.
Добавим год публикации, лицензию и версию игры:
year: 2018,
license: 'MIT',
version: '8.0'
Осталось добавить информацию с описанием нашей игры:
info: 'Небольшая демонстрационная игра на движке protoparser.js'
Итак, мы создали наш первый игровой объект.
var game = {
title: 'Фантазия',
author: 'Алексей Галкин <johnbrown>',
year: 2018,
license: 'MIT',
version: '8.0',
info: 'Небольшая демонстрационная игра на движке protoparser.js'
}
Вы можете добавлять в объект game
только те свойства, которые считаете нужными. Мы могли бы не указывать версию игры или даже название. Соответствующие поля бы просто не отображались в заголовке игры. Однако, хотя, с технической точки зрения, свойство game.title
не обязательно рекомендуется его добавить для избежания возможных проблем с загрузкой и сохранением (при наличии нескольких безымянных игр).
Объект 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
представляется более простым способом настроить поведение команды в своей игре, чем написание соответствующего метода-перехватчика, но только в том случае, если ваша цель – заменить выводимый командой текст на свой, не изменяя поведения самой функции.
Протопарсер, следуя традиции классических текстовых игр, использует стандартный интерфейс командной строки. Если вы работали в терминале значит вы уже с ним знакомы.
Командная строка представляет собой элемент интерфейса, куда пользователь вводит команды. В свою очередь, командная строка состоит из нескольких элементов: т.н. «приглашения командной строки», курсора и, собственно, поля ввода.
Аналогично тому, как мы меняли значения стандартных сообщений системы мы можем устанавливать требуемый вид элементов командной строки. По-умолчанию, поле ввода пусто, но вы можете сделать, чтобы в нем все время или при наступлении определенных условий отображался какой-то текст, как если бы он был введен пользователем. При этом, пользователь имеет возможность выполнить предложенную команду, нажав клавишу «Enter» или отредактировать ее.
commandTemplate: 'готовая команда'
Это может быть полезно, например, если вы хотите сделать в своей игре режим обучения или дать игроку прямую подсказку.
Символ «>» – «приглашение командной строки» отображается слева от поля ввода. При желании, вы можете заменить символьное представление этого элемента.
prompt: '=> '
Более того, присваивая game.prompt
значения игровых переменных, вы можете организовать в командной строке статус-бар – строку, содержащую информацию о состоянии персонажа, игры, мира. Например, номер текущего хода, число заработанных очков, здоровье персонажа и т.п. О том, как это сделать будет рассказано в разделе 12.7 «Строка состояния».
Если вы хотите, чтобы какой-то элемент вовсе не выводился на экран присвойте ему пустое значение.
prompt: ''
Теперь символ «>» не будет выводиться в командной строке.
В протопарсере реализована система т.н. «горячих клавиш» (хоткеев): ввод команды нажатием одной клавиши. Наиболее часто используемые команды уже имеют свои горячие клавиши (см. Приложение 1).
Вы можете менять стандартное поведение горячих клавиш, назначая на них свои команды. Представим, что в нашей игре у персонажа есть карта, в которую приходится постоянно заглядывать. Облегчим жизнь игроку, «повесив» команду ОСМОТРЕТЬ КАРТУ на клавишу «0» цифровой клавиатуры.
num0Key: 'осмотреть карту'
Если вы меняете назначения горячих клавиш в своей игре крайне желательно указать об этом в игровой справке.
Не все клавиши можно использовать в качестве хоткеев. Свойства, соответствующие доступным горячим клавишам перечислены в Приложении 2. Каждое свойство может содержать только одну команду.
У объекта game
есть и другие свойства о которых будет рассказано в следующих главах. Вы также можете обратиться к Приложению 2. «Стандартные свойства объектов» в котором перечислены вообще все стандартные свойства игровых объектов.
Всякий раз, когда игрок вводит команду протопарсер анализирует ее, после чего, при определенных условиях, команда попадает в «историю команд».
История команд – это массив game.commandHistory
, содержащий команды которые вводил пользователь. По-умолчанию, в массиве может храниться до 10 команд, однако автор игры может установить другое число, изменив значение свойства game.commandHistoryLength
. История сохраняется даже после повторной загрузки игры.
C помощью клавиш «стрелка вверх» и «стрелка вниз» можно быстро добавить в строку ввода недавнюю команду. При нажатии на стрелку вверх команды добавляются в строку ввода последовательно: от последней введенной к более ранней. При нажатии на стрелку вниз добавление команд происходит в обратном порядке. Выбранная команда не выполняется автоматически, при желании, игрок может отредактировать ее.
Не все команды, которые вводит пользователь попадают в историю команд. Во-первых, в истории не может быть двух одинаковых команд. Во-вторых, парсер должен полностью распознать команду. В-третьих, объект для действия должен присутствовать в локации. Кроме того, в историю не попадают команды ИСТОРИЯ и ПОВТОРИТЬ. Если в истории уже нет места для новой команды, то самая ранняя удаляется, освобождая место для новой. Игрок может посмотреть все сохраненные команды с помощью команды ИСТОРИЯ.
Автор может отключить историю команд, установив значение game.commandHistoryLength
равным нулю.
У пользователя есть возможность автоматически выполнить последнюю команду, сохраненную в истории команд. Для этого существуют команда ПОВТОРИТЬ или П. Стоит заметить, что если автор отключил в своей игре ведение истории, то данная функция будет недоступна.
В игре персонаж пользователя представлен специальным объектом – player
. Этот объект очень похож на объект типа thing
(мы познакомимся с ним позже), однако имеется и ряд отличий. Объект player
, как и объект game
обязательно должен присутствовать в игре, поэтому давайте сразу его создадим.
var player = {
nam: ['вы', 'себя', 'себе', 'себя', 'собой', 'себе'],
desc: 'Вы - обычный турист, неизвестно как попавший сюда.',
hidden: true,
loc: 'hall'
}
Все свойства, которые мы добавили объекту player
присутствуют и у предметов (thing
), а некоторые – и у комнат (room
). Давайте подробно рассмотрим каждое из этих свойств.
Свойство nam
представляет собой массив – упорядоченную последовательность элементов, в данном случае, имен объекта.
Первые шесть элементов nam
– склонения объекта по падежам (и.п., р.п., д.п. в.п., т.п., п.п.). Эти значения используются при выводе имени объекта. За ними могут следовать произвольное количество имен-синонимов.
Следите, чтобы у объектов не было одинаковых идентификаторов и элементов nam
. Каждый элемент должен состоять строго из одного слова.
В значении свойства desc
хранится описание объекта. Если вы введете команду ОСМОТРЕТЬ СЕБЯ, то игра выдаст «Вы - обычный турист, неизвестно как попавший сюда.» Если вы введете ОСМОТРЕТЬ <ПРЕДМЕТ>, то получите значение свойства desc
этого предмета. Это свойство присутствует и у объектов типа room
и содержит, как не сложно догадаться, описание локации.
Свойство loc
определяет локацию, в которой находится предмет. В нашем случае, это свойство содержит значение hall
– идентификатор соответствующей локации. Таким образом, игрок начнет свое приключение именно в этом локации.
По-умолчанию, названия предметов, находящихся в локации, добавляются в ее описание. В общем случае, это выглядит так:
> осмотреться
Вы стоите в маленькой комнате с окном.Здесь есть кровать, молоток и телефон.
Обычно, если мы уже описали объект в свойстве desc локации нам не нужно, чтобы имя объекта выводилось повторно. Чтобы скрыть имя объекта, находящегося в локации, применяется свойство hidden
. Использование этого свойства не приведет к тому, что объект станет недоступным или мы не сможем выполнить с ним какие-либо действия. Единственное его назначение – не отображать объект в описании локации.
Объект player
, по-умолчанию, отображается в локации, как телефон и кровать в нашем примере. Если мы «забудем» добавить ему свойство hidden
, то в предыдущем примере результат будет следующий:
Здесь есть вы, кровать, молоток и телефон.
Скроем упоминание о персонаже игрока («вы»).
hidden: true
Кроме имени, свойство hidden
скрывает описание объекта в локации (свойство sceneDesc).
Если фраза «Здесь есть: ...» кажется вам не очень художественной вы можете генерировать собственные описания с помощью методов before All
и afterAll
объекта events
. Об использовании этих методов будет рассказано в главе 11.
В протопарсере реализована система инвентаря. Однако, объект player
, как и прочие объекты, не может непосредственно содержать в себе другие объекты. Механизм «принадлежности» одного объекта другому реализуется через свойство loc
объекта. Значением loc
объекта player
является идентификатор стартовой локации. Соответственно, значением loc
предмета является либо значение идентификатора локации, в которой он находится, либо значение 'player'
. В последнем случае это и будет означать, что объект находится в инвентаре.
Если вы хотите, чтобы игрок начал свое приключение не с пустыми руками, установите свойство loc: 'player'
предметам, которыми вы собираетесь снарядить игрока.
Надетые предметы тоже считаются частью инвентаря и отображаются в нем как «надетые».
Иногда вам может потребоваться узнать сколько предметов несет игрок. Сделать это можно с помощью функции getObjByKV
:
getObjByKV('loc','player') // => массив объектов, свойство loc которых равно player
Функция принимает два аргумента: свойство объекта и его значение, и создает «фильтр» или массив объектов, удовлетворяющих нашему запросу.
В нашем примере функция вернет массив всех объектов в игре свойство loc
которых равно 'player'
, т.е. тех, которые находятся в инвентаре. Отлично, скажите вы, но как нам узнать их количество? С помощью метода length
:
getObjByKV('loc','player').length // => количество предметов в инвентаре
По-умолчанию, игрок может носить с собой неограниченное количество предметов, однако вы можете установить некий предел.
Свойство maxCarried
устанавливает максимальное число предметов, которое может быть в инвентаре. Добавьте в объект player
строку:
maxCarried: 3 // не больше трех предметов
Теперь, при попытке взять предмет, если игрок уже несет три предмета (включая надетые), игра выдаст сообщение «Вы несете слишком много вещей.»
Вы в любой момент можете изменить maxCarried
. Предположим, у игрока появился рюкзак. Добавим ему немного «свободного места»:
player.maxCarried = 10 // теперь лимит 10
Свойство maxCarried
может быть только у объекта player
.
В своих приключениях персонажу игрока предстоит перемещаться по игровому миру. Неважно, будет ли этот мир представлять целую вселенную или пару комнат, строиться он будет по одному и тому же принципу.
В прошлой главе мы решили, что игрок начнет свое приключение в локации hall
. Давайте создадим для него эту локацию.
var hall = {
spec: 'room',
head: 'Длинный коридор',
desc: 'Серые каменные стены коридора, кажется, покрыты пылью многих веков.
На севере расположена невысокая дверь. Каменная спиральная лестница
поднимается высоко вверх.',
n: 'round',
u: 'tower'
}
Кое-что, нам уже знакомо из предыдущих глав (свойства spec
и desc
). Рассмотрим типичные свойства объектов типа room
.
Объекты типа 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'
Кстати, вам не обязательно делать выход на юге, если у смежной комнаты вход (или выход) расположен на севере. В конце концов, между комнатами может существовать петляющий туннель. Выход может вести вообще в любую локацию. Однако, большинство игроков будут рассчитывать, что если они вышли из комнаты на север, то вернуться в нее должны, двигаясь на юг. Стоит это учитывать.
У объекта room
есть одно полезное свойство (вообще, у этого объекта не очень много свойств, так что хорошо, что хоть какое-то из них полезное). Это свойство visits
.
Когда мы запускаем игру у всех комнат (кроме стартовой) это свойство отсутствует (undefined
), но стоит игроку побывать в какой-то локации, как свойство visits
этой локации принимает значение «1». При каждом следующем заходе оно будет увеличиваться на единицу.
Свойство visits
удобно использовать, когда нужно вывести какое-то сообщение, когда игрок первый раз попадает в комнату, и в дальнейшем его не показывать.
Помимо описания локация может иметь название. Оно задается через свойство head
.
var tower = {
head: 'Башня'
}
Название локации выводится только когда игрок перемещается в нее.
Объект room
можно использовать как для создания локации, так и для создания «экрана» для вывода произвольного текста, например, вступлений к главам и т.п. В последнем случае, свойство head
не следует задавать. Однако, если вы создаете локацию очень желательно добавить ей свойство head
. Дело в том, что движок ведет список (объект visitedLocs
) посещенных локаций и осмотренных предметов, которые в ней находились на момент последнего посещения (можно посмотреть с помощью команды ЛОКАЦИИ). Если у локации отсутствует свойство head
она не попадет в этот список.
Замечу, что свойство visits
присутствует у всех объектов типа room
, в которых побывал персонаж игрока, даже если у них нет свойства head
.
Почему-то многие люди (в основном, преподаватели математики и авторы движков для текстовых квестов) любят приводить примеры с яблоками. Я их (яблоки) тоже люблю, (как и примеры), поэтому пусть в нашей первой локации будет лежать яблоко.
var apple = {
spec: 'thing',
nam: ['яблоко', 'яблока', 'яблоку', 'яблоко', 'яблоком', 'яблоке', 'фрукт',
'плод'],
desc: 'Ярко-красный плод так и манит его съесть.',
takeable: true, edible: true,
loc: 'hall'
}
Как видите, у нас появилось много новых свойств. Давайте их рассмотрим.
Свойство gend
определяет род и число имени объекта. Это свойство может принимать следующие значения: m
– мужской род, f
– женский род, n
– средний род, p
– множественное число.
До сих пор мы не использовали в нашей игре свойство gend
. Все дело в том, что движок автоматически определяет род (число) для объектов thing
и player
при старте игры. Таким образом, у всех объектов указанных типов есть свойство gend
.
Протопарсер определяет род и число объекта по окончанию его основного имени (свойству nam[0]
).
Таблица соответствия рода (числа) объекта и окончания его имени
Род (число) объекта | Окончание основного имени объекта |
---|---|
Мужской | все символы, не относящиеся к окончаниям женского, среднего рода и множественного числа |
Женский | а, ь, я |
Средний | о, е |
Множественное число | ы, и |
Данное правило, однако, работает не всегда, поэтому при сознании объекта следует сверяться с таблицей. Если род (число) объекта не совпадают с указанным в таблице следует самостоятельно добавить в объект свойство gend
с правильным значением. В главе 12.6 есть пример, когда мы задаем свойство gend
объекту gate
.
Движок использует для вывода имени предмета только первые шесть элементов свойства nam
, которые, являются склонениями основного имени объекта. На него и следует ориентироваться при выборе рода или числа.
Прочитав описание яблока, игрок наверняка решит его съесть. Тут нам и пригодится свойство edible
. Теперь при вводе команды СЪЕСТЬ ЯБЛОКО игра выдаст «Вы съедаете яблоко.» При этом, яблоко исчезнет из инвентаря игрока. Иначе говоря, оно будет удалено из игры. Что же произойдет, если игрок вздумают попробовать на зуб, например, дверь? На все попытки съесть объект без свойства edible: true
, игра неизменно будет отвечать «<Объект> нельзя употребить в пищу.»
По-умолчанию, все предметы жестко «закреплены» в своих локациях. Это сделано потому что большинство предметов в парсерных играх обычно составляют статичные «декорации». Если вам нужно, чтобы игрок мог взять предмет, добавьте ему свойство takeable: true
. Не забудьте добавить takeable
предметам, с которыми игрок начнет игру, иначе, выбросив их, он уже не сможет их вновь поднять.
Из предыдущих глав мы уже многое узнали о свойстве loc
.
До сих пор мы имели дело с предметами, которые располагались в какой-то одной локации. Но, иногда может потребоваться разместить предметы сразу в нескольких локациях, например, реку, стену, звезды, и т.п. Чтобы не создавать дублирующие объекты, достаточно прописать в свойстве loc
все локации, в которых данный предмет будет присутствовать.
В описании коридора и башни мы упомянули лестницу, которая их связывает. Добавим ее в игру.
var stairs = {
spec: 'thing',
nam: ['лестница', 'лестницы', 'лестнице', 'лестницу', 'лестницой',
'лестнице', 'ступеньку', 'ступень'],
desc: 'Узкая винтовая лестница, на которой с трудом разминутся даже два человека, соединяет башню и коридор.',
hidden: true,
loc: ['hall', 'tower']
}
Теперь, игрок может получить доступ к лестнице как находясь в коридоре, так и стоя на башне.
Таким же образом добавим в коридор и круглую комнату стену.
var wall = {
spec: 'thing',
nam: ['стена','стены','стене','стену','стеной','стене', 'пыль', 'грязь',
'камень', 'камни'],
desc: 'Стена сложена из огромных серых камней, первоначальный цвет которых
не представляется возможным определить из-за толстого слоя многовековой
пыли и грязи, которыми они покрыты.',
hidden: true,
loc: ['hall', 'round']
}
Размещать предметы в нескольких локациях следует с осторожностью, ведь изменение состояния предмета в любой из локаций автоматически отразится в остальных. В большинстве случаев, имеет смысл располагать предмет в нескольких локациях, только если его состояние (свойства) ни при каких условиях не может измениться (как в примере с лестницей и стеной).
До сих пор мы не потрудились дать игроку объяснение о том, где он очутился, что происходит и что, вообще, ему следует предпринять. Исправим эту ошибку, добавив в игру билет.
var ticket = {
nam: ['билет', 'билета', 'билету', 'билет', 'билетом', 'билете',
'прямоугольник', 'текст'],
spec: 'thing',
desc: 'Картонный прямоугольник голубого цвета, на котором изящными
золотистыми буквами напечатан какой-то текст.',
text: '«Уважаемый посетитель # 3677! Мы рады приветствовать вас в Фантазии.
Пожалуйста, ни чему не удивляйтесь и чувствуйте себя как дома. Но, если
вам и впрямь надо вернуться домой используйте шляпу».',
loc: 'round',
takeable: true
}
Вы, конечно, заметили новое свойство text
. Само его наличие говорит о том, что предмет можно ПРОЧИТАТЬ. Значение же свойства, как не трудно догадаться, является тем текстом, который будет выведен в ответ на команду игрока ПРОЧИТАТЬ <ПРЕДМЕТ>.
Отлично, кое-что уже прояснилось. Хотя, на самом деле, игрок получил скорее больше вопросов, чем ответов: что это за Фантазия: какая-то страна, парк развлечений? Как он попал сюда? Почему он не должен ничему удивляться, и что это за шляпа, которую он, очевидно, должен найти? Впрочем, на последний вопрос мы как раз сейчас ответим.
var hat = {
spec: 'thing',
nam: ['шляпа', 'шляпы', 'шляпе', 'шляпу', 'шляпой', 'шляпе'],
desc: 'Серая широкополая шляпа выглядит весьма помятой.',
loc: 'tower', // шляпа будет лежать в башне, где игрок не сразу ее найдет
takeable: true
}
Если помните, мы пообещали игроку вернуть его домой, если он использует шляпу. Мы только забыли сказать, как он должен ее использовать. Думаю, вы со мной согласитесь, что наиболее очевидный способ «использовать» шляпу – НАДЕТЬ ее. Сделаем нашу шляпу «надеваемой»:
worn: false
Этой короткой строкой мы сообщили интерпретатору сразу две вещи о нашем предмете: во-первых, что его можно надевать (присутствует свойство worn
), во-вторых, что предмет не надет (false
). Если вы делаете предмет «надетым» (worn: true
) не забудьте сразу поместить его в инвентарь (loc: 'player'
).
Секреты шляпы на этом не заканчиваются. Мы еще к ней вернемся в главе «Объект events», а пока рассмотрим еще несколько свойств.
Сложно себе представить текстовый квест без запертых дверей и спрятанных ключей. Конечно, в нашей игре тоже будут двери. Давайте создадим дверь между коридором и круглой комнатой.
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
и игрок сможет его ОТКРЫТЬ и ЗАКРЫТЬ.
Обычно, предметы в игре лежат там, куда положил их автор и спокойно дожидаются, когда игрок удостоит их своим вниманием. Протопарсер может отслеживать менял ли предмет свое начальное местоположение. Для этой цели служит свойство moved
. У «нетронутых» предметов оно отсутствует. Если предмет был перемещен (не обязательно игроком) он приобретает свойство moved
, которое становится равным 1. Кстати, свойство moved
также добавляется объекту player
как только персонаж игрока переместился в другую локацию. Благодаря этому можно отслеживать сколько передвижений совершил персонаж.
В прошлой главе мы рассмотрели свойство visits
объекта room
. У предметов есть похожее свойство examined
. Каждый раз, когда пользователь осматривает какой-то предмет, значение свойства examined
этого предмета увеличивается на единицу. Таким образом, можно определить сколько раз игрок осматривал предмет. Если предмет не был осмотрен ни разу значение свойства будет равно undefined
.
Свойство examined
удобно использовать, когда нужно вывести определенный текст при первом осмотре предмета.
В инвентаре бывает удобно «хранить» объекты, которые всегда доступны персонажу игрока. Это могут быть как системные объекты, так и игровые, например, аура, внутренний голос, киберимплант, жучок... В таких случаях может возникнуть необходимость скрыть предмет, находящийся у игрока, но не являющийся «инвентарем» в прямом смысле этого слова. Для этого существует свойство hiddenPossession
.
Если значение свойства равно true
объект не будет выводится в списке предметов, находящихся в инвентаре. В остальном же он будет вести себя как обычный предмет в инвентаре. Если вы решите проверить размер инвентаря с помощью функции getObjByKV('loc','player').length
, то обнаружите, что скрытый объект занимает в инвентаре место наравне с обычными видимыми объектами. Стоит это учитывать, если в свой игре вы решите ограничить размер инвентаря с помощью свойства player.maxCarried
. Стоит учитывать и тот факт, что игрок может попробовать взаимодействовать со скрытым объектом.
> Бросить ауру
Вы оставляете здесь ауру.
Чтобы объекты правильно реагировали на определенные действия пользователя необходимо добавить им методы. О методах будет рассказано в главе 9.
Существует три основных способа сообщить пользователю о том, что предмет находится в локации.
Во-первых, можно дать упоминание о предмете прямо в описании локации (следует также добавить ему свойство hidden
). Это неплохой способ для статичных объектов, которые не меняют своего положения, но он не подходит для «мобильных» объектов.
Можно поступить проще – не добавлять описание предмета в локации. В этом случае свойство hidden
добавлять не надо. Движок автоматически добавит в конце описания локации перечень объектов, которые находятся в ней. Плюс данного способа – в описании локации будут присутствовать только те предметы, которые в ней реально есть. Ну, а минус – может пострадать художественная сторона игры.
Наконец, третий способ – использовать свойство sceneDesc
. В качестве значения мы даем описание объекта в контексте описании локации. Проще говоря, то, какую информацию об объекте игрок получит из описания локации. При использовании данного свойства имя объекта не попадает в список «Здесь есть», появляющийся после описания локации.
У нас уже есть яблоко, так что теперь в качестве примера создадим косточку.
var seed = {
spec: 'thing',
nam: ['косточка', 'косточки', 'косточке', 'косточку', 'косточкой',
'косточке', 'кость', 'семя', 'семечко', 'зернышко',
'зерно'],
desc: 'Крохотное золотистое зернышко чуть подрагивает словно живое.
Кажется, ей не очень здесь нравится. Наверное, она хочет туда, где ей
будет хорошо.',
sceneDesc: 'Золотистое зернышко крутится и подпрыгивает на каменном полу.',
loc: '',
takeable: true
}
Обратите внимание, свойству loc
мы задали пустое значение. Таким образом, на начало игры косточка будет недоступна. Сейчас это не важно, позднее мы еще вернемся к этому примеру.
Теперь, если косточка окажется на полу, осмотревшись, пользователь получит сообщение: «Золотистое зернышко крутится и подпрыгивает на каменном полу». Гораздо лучше, чем простое «Здесь есть: косточка».
Важно помнить, что если у объекта есть свойство hidden: true
, то ни имя объекта, ни значение его свойства sceneDesc
не будут присутствовать в описании локации.
Пользователь волен вводить любой текст в строку ввода. Задача парсера проста – понять, в меру его скромных способностей, что человек по ту сторону экрана хотел сказать игре.
Интерфейс командной строки протопарсера прост, он оперирует всего двумя понятиями: глагол и существительное. Команда пользователя может состоять либо из одного глагола, либо из глагола и существительного. Ни больше, ни меньше.
После того как пользователь ввел команду, парсер разбивает ее на части, которые затем анализирует. На первом этапе проверяется длина команды (не больше двух слов и не меньше одного). Если команда прошла проверку, движок проверяет введенный глагол, поскольку, при любых условиях, он обязан присутствовать в команде. В протопарсере уже есть набор стандартных глаголов, каждый из которых ассоциирован с определенной функцией-обработчиком и для которых имеется инструкция по обработке данной команды. Если введенная пользователем команда не содержит глагол, либо он не известен программе, она выдаст стандартное сообщение «Команда непонятна».
ОК, представим невозможное: программа поняла глагол и существительное (если оно было), что происходит дальше?
В игру вступает функция choiceHandler
. Прежде всего, она проверяет существительное рядом с глаголом: введено ли оно, есть ли в текущей локации объект с таким именем, требуется ли для данного глагола существительное вообще?
Описанные выше процедуры выполняются всегда, автор не может их отменить или изменить. А вот дальше наступает самое интересное. Интерпретатор последовательно выполняет серию проверок, пытаясь найти в определенных объектах метод, который обработает команду (перехватит ее у функции-обработчика). Имя искомого,«метода-перехатчика» должно совпадать с именем функции-обработчика команды (см. «Стандартные функции-обработчики (API)»).
Методы создает автор игры. По-сути, это небольшие кусочки кода, которые обрабатывают определенную команду пользователя при наступлении определенных условий. Подробнее о методах будет рассказано в следующей главе. Сейчас же следует знать, что метод, обычно, возвращает логическое значение (true/false
).
Метод возвращает true, если команда пользователя обработана, и false – если нет.
Что означает «возвращает...» и «команда пользователя обработана»? Помимо методов, которые пишет автор существуют стандартные функции-обработчики. Если автор не написал метода для какой-то команды, то ее обработкой займется стандартная функция. Однако, иногда, после того как интерпретатор выполнил какой-то метод, необходимо, чтобы стандартная функция завершила обработку. Иначе говоря, метод должен сообщить движку, что обработка команды не завершена (return false
). Если же метод все сделал сам, и стандартной функции-обработчику беспокоиться ни о чем не надо, он возвращает true
(return true
).
Вообще говоря, если обработка команды не завершена инструкцию return false
писать не обязательно. Не найдя инструкции return true
интерпретатор поймет, что команда не обработана и продолжит обработку.
Теперь, когда мы кое-что узнали о методах, перейдем к цепочке проверок через которые проходит команда пользователя. Важно отметить, что первые четыре пункта (без номеров, см. ниже) срабатывают только один раз – при загрузке страницы с игрой. В дальнейшем, при соблюдении определенных условий (см. ниже), после ввода команды пользователем каждый цикл проверок начинается с п.1.
PARTICIPLE_SUFFIX
, содержащий окончания для краткой формы причастий.game
свойств, содержащих системные сообщения, установленные автором игры. Если какие-то из этих свойств отсутствуют, объекту game
добавляются соответствующие свойства, содержащие стандартные сообщения системы.init
объекта events
. Стоит отметить, что метод init
не требует инструкции return
.desc
объекта, имя которого содержит свойство player.loc
.Следующая цепочка проверок проводится при соблюдение всех следующих условий: команда содержит глагол и он понятен системе; команда состоит не более чем из двух слов; существительное либо присутствует либо отсутствует в команде, в соответствии с тем, какой глагол, стоит перед ним; предмет для действия находится в текущей локации или в инвентаре.
game.turn
на единицу.game.commandHistory
(исключения: команды ПОВТОРИТЬ, ИСТОРИЯ, а также, команды уже находящиеся в истории команд в массив game.commandHistory
не попадают). Если число команд в массиве при этом превысит значение game.commandHistoryLength
последний элемент массива удаляется.head
, ее идентификатор добавляется в качестве имени свойства объекта visitedLocs
. Свойству присваивается массив имен объектов (свойство nam[3]
), находящихся в этой локации, у которых свойство examined
не равно undefined
.beforeAll
у объекта events
, и если есть, то выполнит его. Если метод вернет true
, интерпретатор перейдет к п.9.true
, интерпретатор перейдет к п.9, если false
– к п.8.true
, интерпретатор перейдет к п.9, если false
– к п.8.globalVerbs
. Если метод вернул true
, интерпретатор перейдет к п.9, если false
– к п.8.globalVerbs
, ни в текущей локации, ни в объекте действия, то, если в объекте game
есть свойство с именем вызываемой функции выводит значение этого свойства, иначе вызывается стандартная функция-обработчик.events
метод afterAll
, и, если есть, то вызовет его.После ввода любой команды значение game.commandHistoryIndex
становится равным -1.
Если в вашей игре отсутствуют объекты events
или globalVerbs
движок на этапе загрузки игры создаст соответствующие пустые объекты events
и globalVerbs
. Это необходимо для корректной работы приложения.
Что произойдет, если метод присутствует у нескольких объектов? Будет выполнен тот, который первым расположен в цепочке.
Подробнее об объектах events
и globalVerbs
будет рассказано в соответствующих главах.
Стандартные обработчики команд – это хорошо. Но, иногда вам может потребоваться написать для какого-нибудь действия нестандартную реакцию. К счастью, протопарсер позволяет это сделать. По-умолчанию, любой предмет можно выбросить из инвентаря. Давайте сделаем наш билет «невыбрасываемым».
Для этого добавим в объект 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
, которая выводит строку «Вы говорите 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
.
Объект 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
работают по тем же правилам, что и методы других объектов.
Объект 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
.
В данной главе мы продолжим писать нашу игру, вооружившись знаниями, полученными в предыдущих главах. Здесь будет не очень много теории, а основное внимание будет уделено практической реализации некоторых игровых деталей. Вы увидите, как на движке протопарсера можно реализовать игровых персонажей, диалоги, динамическое изменение локаций и многое другое.
В разделе «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
}
А, теперь, вернемся к шляпе. После того как мы указали игроку на шляпу как путь к победе (и тут же ее отняли) он, скорее всего, захочет завладеть шляпой, а для этого ему придется каким-то образом слезть с башни, ведь, если помните, никаких выходов наружу из коридора внизу у нас нет. Игрок может попробовать пойти из башни прямо на север к озеру, куда улетела шляпа.
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
}
Однако, у него ничего из этого не выйдет, поскольку мы перехватили все соответствующие стандартные функции.
Хорошо, но как же игроку все-таки спуститься? Не забыли про яблоко, которое мы любезно оставили игроку? Если помните, когда мы съедали плод, у нас в руках оказывалась косточка (волшебная, как вы, конечно, догадались). Сделаем так, что, если игрок бросит косточку с башни, из земли вырастет дерево, по которому можно будет спуститься вниз.
Добавим косточке метод 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
}
Ну, хорошо, мы уже много где побывали, научились работать с локациями и предметами, и, даже посадили дерево. А как же другие персонажи? Неужели наш турист обречен на одинокое скитание? Разумеется, нет! Компанию нашему герою составит старик-лодочник. Создадим вначале поляну, на которой он будет «обитать»:
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])
}
}
Начну с плохой новости. Поскольку протопарсер умеет распознавать только глагол и существительное, то сложные фразы, типа, ЛЛОЙД, ОТКРОЙ ХОЛОДИЛЬНИК И ПРИНЕСИ МНЕ ПИВА он просто не поймет. В протопарсере общение игрока с персонажами реализуется через ввод ключевых слов: СКАЗАТЬ ПРИВЕТ, СПРОСИТЬ ЛОДКУ, ОТВЕТИТЬ ПАРОЛЬ, и т.д.
Давайте научим дедушку понимать и отвечать на наши вопросы. Добавим объекту 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]) + ' что надо!»')
}
Хорошо, но откуда игрок возьмет монету? Если помните, у нас осталась «неиспользованной» еще одна локация – выход из парка.
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
}
В разделе 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
? В этом случае мы увидили бы изменение строки приглашения только через один ход.
Ну, вот, мы почти и подошли к финалу нашего маленького приключения. Добавим автомат по продаже билетов и монетку.
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-тэгах и форматировании будет рассказано в главе «Форматирование вывода».
Вот мы и подошли к финалу нашей истории. Игрок доплыл до середины озера, взял шляпу, прочитал текст на ленте. Он уже примеривает на себе головной убор, и набирает на клавиатуре заветное слово... Нам остается добавить несколько строчек кода в метод afterAll
:
if (com == 'say' && optional == 'домой' && hat.worn) {
p('Едва вы произнесли это слово, как, внезапно, обнаружили, что
сидите уставившись в экран. Ха-ха! Это была всего лишь игра. Спасибо,
что уделили ей время. Приходите еще!')
end(1)
}
Ваша игра может закончиться хорошо (как наша) или не очень, или вообще не закончиться, если в ваш код вкралась ошибка. Функция end
определяет где и как завершится ваша игра. Вы можете вставить ее в любое место в своем коде. Но, как только до нее дойдет очередь игра будет тут же остановлена. После остановки игры командная строка вместе с курсором исчезнут, и пользователь уже ничего не сможет ввести. Вы можете явно сообщить игроку об окончании игры, если вызовите функцию end
с аргументом: 0
– если игрок проиграл, 1
– если выиграл. Если game.noScore
не равно true
, пользователь увидит итоговое количество сделанных им ходов и число набранных очков.
Если вы вызовите end
без аргументов, на экран ничего выведено не будет, и игра просто закончится.
В протопарсере реализован механизм мульти-сохранения и загрузки игр. Для корректной работы этой функции необходимо, чтобы браузер предоставлял приложению доступ к объекту 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
– для отмены.
Ваша игра может быть очень большой и сложной, но это не должно означать долгое и сложное тестирования. Для автоматизации этого процесса в протопарсере реализован механизм автотестирования. Пользоваться им очень легко.
Откройте файл tests.js
. Команды для тестирования последовательно записываются в массив commands
. Для примера, в массиве уже есть несколько команд. Вы можете удалить их и добавить свои. После этого в файле story.js
установите свойство game.tests
равным true
.
Запустите файл index.html
. Тестирование начнется сразу после вывода описания стартовой локации (если есть пользовательские методы, то они тоже сработают). Далее программа будет последовательно вводить команды из массива commands
и выводить результат их выполнения на экран. По окончании тестирования появится сообщение «ТЕСТИРОВАНИЕ ЗАВЕРШЕНО.» Вы можете продолжить вводить команды как обычно.
Вы также можете проверить корректность работы протопарсера на вашем устройстве, запустив из папки examples/Fantasia
файл index.html
с включенным режимом автотестирования.
Благодаря наличию в современных браузерах «режима разработчика», вы можете в режиме реального времени просматривать и изменять значение свойств игровых объектах. Это бывает особенно полезно при дебаггинге.
Чтобы воспользоваться этой функцией:
Иногда в коде встречаются ошибки. Когда интерпретатор js встречает такую ошибку он может просто ее проигнорировать, либо прекратить выполнение. В обоих случаях, игра, на каком-то этапе, может повести себя не так как мы ожидали. И, что еще хуже, браузер не сообщит нам какую ошибку он встретил и где. Однако, все-же есть простой и удобный способ это выяснить. Как вы догадались, это уже знакомый нам «режим разработчика».
Кроме возможности просматривать и изменять переменные вы можете отслеживать все ошибки, возникающие в ходе выполнения программы.
Вы, наверное, уже поняли, что «режим разработчика» является практически заменой традиционного IDE. Фактически, вы можете писать игру в браузере. Выбирайте способ, который вам кажется проще и удобнее.
Большинство авторов, пишущих «парсеры», включая признанных мастеров жанра, в своих играх уделяют гораздо больше внимания содержанию, чем оформлению. Возможно, это всего лишь дань традиции, берущей свое начало с той поры, когда основным средством взаимодействия между человеком и компьютером была командная строка. Не исключено и то, что за несколько десятилетий существования жанр выкристаллизовался, избавившись от всего лишнего, и, можно сказать, стал самодостаточным.
Как бы то не было, являетесь ли вы последователем «старой школы» или вам по душе творческие эксперименты, в протопарсере вы можете полностью настроить «внешний вид» игры «под себя». Для этого всего лишь нужно отредактировать файл стилей 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'
}
В предыдущей главе мы рассмотрели лишь крохотную часть тех возможностей, которые предоставляет 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» ссылка открылась в новом окне. Мы также изменили цвет ссылки на коричневый для того, чтобы она лучше читалась, но, при этом, не слишком отвлекала на себя внимание. Вы можете устанавливать на ссылки события, прятать их, и т.п. Таким образом, можно, например, сделать в своей игре главное меню.
В этой главе мы использовали т.н. «встроенные стили». Вы можете встраивать в свою игру не только стили, а вообще любые элементы: картинки, музыку, формы, и, даже таблицы.
Если вы чувствуете, что ваша игра выиграет, если в ней будут картинки и музыка – эта глава для вас.
Добавим красивый разделитель после интро.
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>
, если необходимо импортировать библиотеку из внешнего файла.
Вообще говоря, каждая команда состоит из двух частей: параметров, определяющих вызов командной функции и самой функции, определяющей поведение команды. Все стандартные команды единственное назначение которых – вывести статичный текст являются «виртуальными», т.е., у них есть параметры, определяющие то, как данная команда будет «вызываться», но сама функция, фактически, отсутствует. Ее роль выполняет свойство объекта 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: '– Апч-хи!!!'
}
Сниппеты – это небольшие кусочки кода, которые можно использовать в своей программе (иногда, после небольшой адаптации). В этой главе собраны несколько полезных и интересных, на мой взгляд, сниппетов, которые могут сделать вашу игру более «живой» и интересной. Возможно, некоторые из этих сниппетов или их аналоги когда-нибудь станут частью движка, возможно – нет. Так или иначе, ничто не мешает вам прямо сейчас использовать их в вашей игре.
Многие искушенные в парсерах игроки любят использовать команду ВЗЯТЬ ВСЕ. Это, действительно, удобно, когда можно взять сразу несколько предметов одной короткой командной. К тому же, у некоторых предметов нередко бывают такие длинные названия, что проще написать ВСЕ. Кроме того, с помощью такой команды можно быстро проверить все доступные в локации предметы. Некоторые авторы позволяют игроку использовать команды типа ВЗЯТЬ ВСЕ, другие заставляют указывать конкретные объекты.
В протопарсере пока не реализована корректная обработка команд с существительным «все», однако вы можете добавить в свою игру такую реализацию самостоятельно. Более того, вы можете определить как именно движок будет обрабатывать эту команду.
Ниже приведен пример одного из вариантов такой реализации. В нем мы добавим дополнительные проверки в метод 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']
}
Стандартная функция-обработчик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 | Игровая команда |
Имя свойства | Тип значения | Класс объектов, в котором может присутствовать свойство | Описание | Значение по-умолчанию |
---|---|---|---|---|
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. Системные команды:
|
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 |
Ниже приводится полный код игры «Фантазия», которую мы создавали на протяжении этого руководства в качестве обучающего примера. Код содержится в файле story.js
в папке examples/Fantasia
.
var game = {
title: 'Фантазия',
author: 'Алексей Галкин <johnbrown>',
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('– Апч-хи!!!')
}
Отсутствие необходимости компиляции, однако, требует от автора особой внимательности при написании кода, поскольку в случае ошибок в коде интерпретатор не сообщит о них, и игра будет работать некорректно или просто не запустится. Тем не менее, во многих современных браузерах есть т.н. «режим разработчика», в котором можно посмотреть информацию об ошибке. Подробнее об этом будет рассказано в главе «Тестирование и отладка».↩
Объект localStorage
в вашем браузере должен быть доступен для записи и чтения.↩
Вообще говоря, метод afterAll
может сработать и после beforeAll
, если последний вернет true
.↩
Если введеная команда не может быть передана функции-обработчику, интерпретатор выдаст сообщение об этом, а методы events
не будут вызваны (см. Главу 9 «Обработка команд»).↩
Т.е., ведущая себя, в общем, как обычная дверь, за исключением того, что ею нельзя воспользоваться по прямому назначению.↩
Аргументы в квадратных скобках являются необязательными; функция в квадратных скобках является «виртуальной»↩
Аргументом может быть переменная и выражение при условии, что они преобразуется к допустимому типу аргумента.↩