Метапарсер

Быстрый старт

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

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

Кроме того, полезным может оказаться изучение кода игры «Иван Царевич» с официального репозитория: http://instead-games.sourceforge.net

Создайте каталог с игрой, которую вы разрабатываете. Например, назовем ее условно mygame. В каталоге mygame создайте подкаталог parser и скопируйте в него lua файлы из первой ссылки. Также, в каталог с игрой скопируйте файл morph.dict. Все готово для того, чтобы создать в каталоге mygame файл main.lua.

Хотя, я также я рекомендую скачать анимированный gif курсор, например, отсюда: http://sourceforge.net/p/instead-games/code/HEAD/tree/trunk/metaparser/crossworlds/cursor.gif?format=raw и также положить его в mygame.

Модуль парсера имеет множество настроек:

-- $Name: Моя парсерная игра$
-- $Version: 0.1$
 
instead_version "1.9.0"
 
require "quotes" -- нужные модули для вашей игры
require "dash"
 
require "parser/metaparser" -- загрузка модуля парсера 
 
input.cursor = img 'cursor.gif' -- переопределим изображение курсора на красивый и анимированный
 
morph.debug = true -- эта строка нужна на время отладки вашей игры, после релиза закомментируйте ее
 
parser.cmd_history = true -- разрешить историю команд по клавишам вверх/вниз
parser.history_len = 16 -- длина истории команд (по умолчанию - 100)
parser.empty_warning = '?' -- сообщение-подсказка если текущий ввод содержит ошибки (можно сделать картинкой)
parser.scroll = true -- режим скроллинга как в классических парсерах, по умолчанию включено
parser.scroll_history = 100 -- длина истории вывода -- по умолчанию 100
parser.filter = false -- фильтровать ошибочные слова в командах (игрок не сможет ввести невозможное слово) 
-- (по умолчанию false);
parser.err_filter = false -- фильтровать неверные (незавершенные или с лишними словами) команды. По умолчанию true.
parser.hideverbs = false -- прятать глаголы до тех пор, пока игрок не начнет ввод 
-- (по умолчанию false, ставя настройку в true вы делаете парсер более похожим на классику, 
-- но не даете возможность играть в вашу игру мышкой)
parser.nohints = false -- если поставить в true, подсказок не будет вообще (только автодополнение)
parser.hintinv = true -- выделять курсивом предметы инвентаря, по умолчанию false
 
morph.yo = false -- учитывать букву ё? по умолчанию -- false, то-есть е и ё взаимозаменяемы;
 
morph:init ('main.lua') -- список файлов с игрой, для которых будет создан словарь. 
-- Если в вашей игре несколько файлов, то можно написать что-то вроде: 
-- morph:init ('main.lua', 'game.lua', 'ep1.lua') и т.д.
 
me().person = 1 -- от какого лица ведется повествование. 1 - от первого. Можно задать 1, 2 и 3.
-- me().plural = true обращение на Вы, в случае, если me().person == 2
 
me().Exam = [[Мне не хочется заниматься самоанализом.]]; -- Метод, который описывает главного героя. 
-- Конечно, может быть функцией.
-- parser.compass = true -- стороны света всегда активны (доступны глаголы-стороны света)
-- parser.noway = [[Туда не пройти.]]  -- сообщение о невозможности пройти на сторону света
 
parser:init() -- активировать стандартный набор глаголов
 
main = cutscene { -- титульная страница
        nam = 'Пролог';
        dsc = "Это моя игра на метапарсере!";
        walk_to = 'myroom';
}
 
myroom = room {
        nam = 'Комната';
}

Для корректной работы игры нужна специализированная тема. Главное условие – это наличие широкой горизонтальной области инвентаря, в котором и будет осуществятся текстовый ввод. Пока вы не создали свою уникальную тему, вы можете просто скачать файл theme.ini отсюда: http://sourceforge.net/p/instead-games/code/HEAD/tree/trunk/metaparser/crossworlds/theme.ini, и поместить в mygame/.

Запустите свою игру, потом, находясь в игре, нажмите клавишу «f11», при этом создастся словарь для вашей игры: mygame/game.dict. На самом деле, пока наша игра не содержит объектов словарь не нужен, но в дальнейшем он понадобится (конечно, при условии если в игре будет хотя бы один объект). Периодически обновляйте словарь, нажатием на f11.

В дальнейшем, когда вы закончите разработку игры, вы можете стереть полный словарь morph.dict, закомментировать строку morph.debug = true и запаковать игру в .zip архив. А пока, по мере добавления объектов не забывайте обновлять словарь.

Глаголы

В центре архитектуры метапарсера лежит такая концепция, как глагол. На самом деле, заглянув в каталог mygame/parser вы увидите несколько lua файлов. Один из которых называется stdparser.lua. stdparser.lua реализует некий «средний-универсальный» набор глаголов. Вы уже могли убедиться в этом, когда запустили mygame.

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

Итак, добавление глагола выглядит примерно так:

Verb { "Take %2", "взять|~забрать", "{obj}вн" };

На этом примере, рассмотрим основные особенности глагола.

Глагол, как видим, определяется с помощью функции Verb, в качестве параметра к которой передается таблица-описатель глагола. Первый элемент таблицы это событие. В нашем случае, событием является Take %2. Take – это имя события. %2 – задает параметр события, в данном случае, параметром события является 2-й элемент глагола.

За событием следуют остальные элементы глагола. Назовем их параметрами глагола. Параметры глагола это, фактически то, что сможет набрать игрок (и для чего будут даваться подсказки метапарсером), Например, первый параметр в нашем случае это взять|~забрать. Как видим, первый параметр – это и есть глагол в том виде, в каком его видит/может ввести игрок.

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

Второй параметр глагола в нашем случае выглядит как: {obj}вн. На самом деле, это такой же параметр как и первый, но здесь использована подстановка {obj}падеж. Она означает, что игрок может набрать слово, которое будет обозначать объект сцены. Причем слово должно быть в винительном падеже. Падежи задаются двухбуквенными сокращениями: им, рд, дт, вн, тв, пр. Если вы не понимаете, что означают эти сокращения, лучше пока воздержаться от написания парсерной игры.

Внимание: существует еще один падеж пр2 – это местный падеж или второй предложный. Например: о поле(пр), но на полу(пр2). При этом, если для слова не существует формы пр2, будет сгенерирована форма пр. Используйте пр2 в случае если речь идет о каком-либо месте.

Сокращение Падеж
им Именительный
рд Родительный
дт Дательный
вн Винительный
тв Творительный
пр Предложный
пр2 Местный (2й предложный)

В качестве сокращений можно использовать:

Сокращение Смысл
{obj} Объекты сцены
{inv} Объекты инвентаря
{way} Переходы
{text} Произвольный текст
{объект} Произвольный объект
{} Конец ввода - описано в дальнейшем

Например, мы могли бы изменить глагол так:

Verb { "Take %2", "взять|~забрать", "{obj}вн|{inv}вн" };

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

Подстановку {text} имеет смысл использовать только при реализации диалогов/ввода пароля и в других подобных случаях.

При подстановках объектов, при условии что задан падеж, движок будет формировать и ожидать слово в корректной форме. Для того, чтобы этот механизм работал, атрибут nam (или disp) у объекта должен быть задан специальным образом. Например:

apple = obj {
    nam = _'яблоко|~яблочко|~красное яблоко';
    dsc = [[На полу лежит красное яблоко.]];
};

Как видим, как и в случае с глаголами, атрибут может содержать синонимы (разделенные символом |), которые обычно удобно делать скрытыми (символ ~ в начале слова).

Символ _ в начале строки означает, что при генерации словаря (кнопка f11 при включенном parser.debug) слова атрибута будут добавлены в словарь. Таким образом, всегда ставьте _ для объектов. Если вы хотите оформить атрибут в виде функции, то запись может стать такой:

apple = obj {
    nam = 'яблоко';
    disp = function(s)
        return _'яблоко|~яблочко|~красное яблоко';
    end;
    dsc = [[На полу лежит красное яблоко.]];
};

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

Переменное число параметров

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

Определение нашего глагола может выглядеть так:

Verb { "Attack %2", "ударить|~бить|~разбить|~сломать", "{obj}вн"};

Отлично! Но что если мы хотим дать возможность игроку написать что-то вроде: ударить гоблина мечом. И не только дать это написать, но и понять это в своей игре?

Verb { "Attack %2 %3", "ударить|~бить|~разбить|~сломать", "{obj}вн", "{inv}тв"};

Хорошо! Действительно хорошо? Постойте, теперь у нас пропала возможность написать просто ударить гоблина, так как метапарсер ждет еще одно слово. Что делать? Нам поможет конструкция {}:

Verb { "Attack %2 %3", "ударить|~бить|~разбить|~сломать", "{obj}вн", "{}|{inv}тв"};

Обратите внимание на запись «{}|{inv}тв», она означает, что в данный момент игрок может ввести или предмет инвентаря в творительном падеже, или завершить ввод! Вы можете использовать {} в тех местах, где ввод может быть прекращен.

Допустим, объекту гоблин соответствует goblin, а мечу – sword. Тогда в обработчик Attack goblinа (goblin:Attack()) в качестве второго параметра придет sword, если игрок вписал его.

goblin = obj {
    nam = _'гоблин';
    Attack = function(s, w)
        if not w then
            p [[Гоблина голыми руками не возьмешь!]]
            return
        end
        if w == sword then
           p [[Я размахнулся мечом и прогнал гоблина!]]
           remove(s)
           return
        end
        p [[Это не поможет.]]
    end;
}

Предлоги

До сих пор мы игнорировали предлоги. А ведь часто они нужны! Попробуем, например, реализовать глагол бросить. По русски мы говорим: бросить золото на стол, или – бросить дротик в гремлина. Как реализовать такой глагол?

    Verb { "Drop %2 %4", "бросить", "{inv}вн", '{}|в|на', "{obj}вн"};

Как видим, игрок может написать: «бросить яблоко», «бросить яблоко в гоблина», «бросить яблоко на гоблина» и во всех случаях будет порождено событие Drop, с параметрами %2 и %4. Не забывайте, если первый параметр (в нашем случае %2) – соответствует объекту, то вызовется метод этого объекта.

Параметры как функции

В предыдущем примере мы сделали глагол, который генерирует событие Drop для записей вида: бросить дротик в гоблина и бросить дротик на гоблина. Но что если мы хотим, что бы в зависимости от предлога, генерировались разные события? Скажем DropIn, и DropOn? Дело в том, что любой элемент в записи Verb может быть функцией! Функция должна вернуть текст, который будет интерпретироваться так, как если бы он был написал при декларации Verb. Это делает наши возможности безграничными:

    Verb { function(s)
               if s:word(3) == 'в' then
                   return "DropIn %2 %4"
               else
                   return "DropOn %2 %4"
               end
           end, "бросить|~кинуть", "{inv}вн", '{}|в|на', "{obj}вн"};

Тут мы встретили новую конструкцию s:word. На самом деле, в функцию-параметр первым аргументом (который мы назвали s) всегда приходит объект parser, поэтому мы могли бы написать parser:word(). parser:word() – это функция, которая возвращает текст ввода в позиции, указанной в параметре. В нашем случае, s:word(3) соответствует 3-му слову. бросить дротик в гоблина. То есть - это слово 'в'. Предлог.

Далее, в зависимости от предлога, мы вернет то событие, которое хотим: DropIn или DropOn.

Вы можете использовать parser:word() в своей игре не только в параметрах глагола, но и в обработчиках, если это вам нужно. При этом, если parser:word() вызывается без параметра, функция вернет последнее введенное слово.

Аналогично, вы можете оформить в виде функции и другие параметры. И анализируя прошлый ввод с помощью parser:word() делать ветвления, в зависимости от прошлого ввода. Например, фраза: толкнуть ящик к двери или толкнуть ящик на дверь предполагает, что последнее слово находится в дательном или винительном падеже:

pushobj = function(s)
    if s:word(3) == 'к'  then
        return "{obj}дт"
    elseif s:word(3) == 'на' then
        return "{obj}вн"
    end
end
Verb { "Push %2 %4|Moved %4 %2", "толкнуть|~двигать|~сдвинуть", "{obj}вн", "{}|к|на", pushobj };

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

Например:

Verb { "Swim", function(s)
    if here() == sea then
        return "плыть"
    end
end};

Глагол плыть будет активен только на море.

Все эти приемы позволяют, при желании, разнообразить ввод произвольным образом.

Наборы глаголов

Глаголы, которые добавляются с помощью функции Verb в stdparser.lua или собственные глаголы игры формируют единое пространство глаголов.

Метапарсер, когда ему требуется получить глаголы, использует функцию verbs(). Она реализована следующим образом:

function verbs()
    if not here().verbs then
        if not game.verbs then
            return me().verbs
        end
        return game.verbs
    else
        return here().verbs
    end
end

Как видим, алгоритм этой функции работает следующим образом:

  1. Если в текущей комнате есть свой набор глаголов, использовать только его.
  2. Если у текущего игрока есть свой набор глаголов, использовать только его.
  3. Использовать глобальный набор глаголов из game.

Но как определять глаголы в комнате и у игрока? На самом деле есть два способа. Вы можете просто объявить verbs, в момент инициализации, например:

panic = room {
    nam = 'сон';
    dsc = [[Вы хотите спать.]];
    verbs = {
               { 'Sleep', 'уснуть|~заснуть|~спать' }
            };
}

Либо, вы можете воспользоваться вторым параметром Verb, например:

Verb ({'Sleep', 'уснуть|~заснуть|~спать'}, panic) -- добавить глагол в комнату panic

Имейте в виду, что Verb вы можете использовать только в глобальном пространстве, в init() или в start().

Скрытие подсказок

Вы можете отключить подсказку глаголов с помощью задания parser.noverbs = true (в глобальном контексте, init() или start()). При этом, подсказки появятся только после начала набора. Вы можете изменять это свойство в конкретных комнатах с помощью задания атрибута noverbs у комнаты.

parser.noverbs = true
 
intro = cutscene {
   nam = 'Введение';
   noverbs = false;
   walk_to = 'stage1';
}

События

Итак, игрок набрал фразу (возможно, даже кликая мышкой по ссылкам-подсказкам): взять яблоко. Или: забрать яблоко. Яблоко – идентифицирует объект сцены (подстановка {obj}) и является параметром события. Помните, событие глагола выглядит так: Take %2. %2 здесь соответствует яблоку. И вот, игрок нажимает ввод, что будет происходить дальше?

Источником события в метапарсере является ввод глагола. Набрав: взять яблоко мы спровоцировали событие Take с параметром-объектом яблоко. Здесь следует отметить, что параметры события могут быть объектами или текстом. Если параметр был сформирован с помощью подстановки: {obj}, {inv}, {way} или {объект}, то этот параметр является объектом. Во всех остальных случаях параметром будет просто текст.

Итак, инициировано событие Take с параметром-объектом 'яблоко'. Что происходит дальше?

Метапарсер вызывает цепочку обработчиков. Каждый обработчик это, как обычно, функция или строка. Строка – синоним функции, которая возвращает строку, как обычно в INSTEAD. Цепочка обработчиков может быть прервана в любой момент, если какой-либо из обработчиков вернет непустую строку (или выведет ее с помощью функций p/pr/pn, или обработчик сам является строкой), или вернет true. Исключение составляют лишь after_ обработчики, которые вызываются в любом случае, но о них – позже.

Цепочка обработчиков выглядит следующим образом:

  1. parser.events.before_Событие;
  2. before_Событие у текущей комнаты;
  3. Если первый параметр к Событию является объектом, то вызовем before_Событие у объекта;
  4. Если первый параметр к Событию является объектом, то вызовем Событие у объекта;
  5. Событие у текущей комнаты;
  6. parser.events.Событие;
  7. Если первый параметр к Событию является объектом, то вызовем after_Событие у объекта;
  8. after_Событие у текущей комнаты;
  9. parser.events.after_Событие;

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

Например, разберем случай с Take. Представим себе, что объект яблоко это apple, тогда цепочка будет выглядеть следующим образом:

  1. parser.events.before_Take(parser, apple);
  2. here():before_Take(apple);
  3. apple:before_Take();
  4. apple:Take();
  5. here():Take(apple);
  6. parser.events.Take(parser, apple);
  7. apple:after_Take(apple);
  8. here():after_Take(apple);
  9. parser.events.after_Take(parser, apple);

Запись объект:обработчик означает, что если обработчик оформлен в виде функции, то первым параметром в функцию будет передан объект, а затем – все остальные параметры в ().

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

castle1 = room {
        nam = 'На доске';
        before_Take = [[Я стою на тонкой доске, внизу пропасть, я не хочу потерять равновесие!]];
}

И все, мы просто обрубаем все попытки сделать Take в данной комнате.

Обработчики parser.events.Событие обычно содержат поведение по-умолчанию. Например, в случае Take, обработчик parser.events.Take содержит взятие предмета по-умолчанию. Мы вернемся к этому вопросу позже.

Default обработчик

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

  1. parser.events.before_Take(parser, apple), если обработчика нет, то parser.events.before_Default(parser,'Take', apple);
  2. here():before_Take(apple), если обработчика нет, то here():before_Default('Take', apple);
  3. apple:before_Take(), если обработчика нет, то apple:before_Default('Take');
  4. apple:Take(), если обработчика нет, то apple:Default('Take')
  5. here():Take(apple), если обработчика нет, то here():Default(apple, 'Take');
  6. parser.events.Take(parser, apple), если обработчика нет, то parser.events.Default(parser, apple, 'Take');
  7. apple:after_Take(apple), если обработчика нет, то apple:after_Default(apple, 'Take');
  8. here():after_Take(apple), если обработчика нет, то here():after_Default(apple, 'Take');
  9. parser.events.after_Take(parser, apple), если обработчика нет, то parser.events.after_Default(parser, apple, 'Take');

Сложно? Но на практике, это не так. Например, мы хотим сделать солнце не досягаемым объектом. Как этого добиться?

sol = obj {
        nam = _'солнце';
        Default = [[Солнце слишком далеко.]];
}

Конечно, некоторые вещи все-таки можно делать с солнцем, например, осмотреть солнце:

sol = obj {
        nam = _'солнце';
        Exam = [[Яркое!]];
        Default = [[Солнце слишком далеко.]];
}

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

Any обработчик

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

  1. parser.events.before_Any(parser, 'Take', apple) затем: parser.events:before_Take(apple) или parser.events.before_Default(parser,'Take', apple);
  2. here():before_Any('Take', apple) затем here():before_Take(apple) или here():before_Default('Take', apple);
  3. apple:before_Any('Take') затем: apple:before_Take() или apple:before_Default('Take');
  4. apple:Any('Take') затем: apple:Take() или apple:Default('Take')
  5. here():Any('Take', apple) затем: here():Take(apple) или here():Default(apple, 'Take');
  6. parser.events.Any(parser, 'Take', apple) затем: parser.events:Take(apple) или parser.events.Default(parser, apple, 'Take');
  7. apple:after_Take(apple) или apple:after_Default(apple, 'Take'), затем: apple:after_Any(apple, 'Take')
  8. here():after_Take(apple) или here():after_Default(apple, 'Take'), затем: here():after_Any(apple, 'Take')
  9. parser.events:after_Take(apple) или parser.events.after_Default(parser, apple, 'Take'), затем: parser.events:after_Any(apple, 'Take')

На самом деле, обычно вам не придется задумываться о всем этом, вы просто выбираете подходящие для вас обработчики. Конечно, вы можете игнорировать before/after/default и any обработчики, но с ними многие вещи делаются проще. Например, написав обработчики after_Drop и after_Take вы сможете учитывать вес носимых предметов, а написав функцию after_Any в комнате, фактически, получите возможность реагировать на все команды игрока в этой комнате.

After обработчики

Обработчики 'After работают особым образом. Вся цепочка after вызывается вне зависимости от того, что вернули предыдущие функции и их вывод суммируется. Это сделано для того, чтобы дать возможность отслеживать совершенные команды. Но что делать, если, к примеру, вам хочется реализовать функцию after_Take, которая бы выводила текст по-умолчанию, в случае, если ранее никто в цепочке обработчиков его не вывел? Для этого можно воспользоваться функцией parser:react(), которая возвращает текущую реакцию обработчиков.

parser.events.after_Take = function(self)
    if self:react() then -- нечего делать, вывод уже был
        return
    end
    p [[О.К.]] -- лаконичная реакция на взятие предмета ''по умолчанию''
end

Вы также можете отменить вывод предыдущих обработчиков и вернуть свой. Для отмены вывода используйте: parser:react(false)

Параллельные обработчики

И снова, я вас обманул (в последний раз!). Есть еще одно усложнение, упрощающее написание игр. Представим себе глагол отдать. Игрок отдает яблоко гному. Если руководствоваться вышеизложенным, то глагол можно реализовать примерно так:

Verb { "Give %2 %3", "отдать|~дать|~подарить", "{inv}вн", "{obj}дт" };

Таким образом, в нашем случае можно написать обработчик Give у яблока:

apple = obj {
        nam = _'яблоко';
        Give = function(s, w)
                if w == gnome then
                        p [[Я отдал яблоко гному.]]
                        remove(s, me())
                end
        end
}

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

gnome = obj {
        nam = _'гном';
        Receive = function(s, w)
                if w == apple then
                        p [[Гном принял яблоко.]]
                        remove(w, me())
                end
        end
}

Конечно, мы можем переделать глагол так:

Verb { "Receive %3 %2", "отдать|~дать|~подарить", "{inv}вн", "{obj}дт" };

Но, это будет не универсально, так как мы не сможем комбинировать оба подхода. Как же быть? На сцену выходит конвейерные события.

Verb { "Give %2 %3|Receive %3 %2", "отдать|~дать|~подарить", "{inv}вн", "{obj}дт" };

Да-да, как вы уже догадались, наша схема усложняется снова. Я не буду ее писать, так как ее легче понять, чем полностью формализовать. Суть в том, что события Give и Receive идут параллельно друг-другу шаг за шагом, строка за строкой. Конечно, вы можете не использовать эти механизмы, если они вам не нужны. Что касается Give, то он уже реализован в stdparser.lua за вас, как и многие другие глаголы.

Синонимы и перенаправления

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

Как это сделать? Первый вариант, написать обработчики Take, Push и Pull с одинаковой реакцией. Конечно, это сработает, но будет громоздко. В метапарсере в таких случаях лучше использовать синонимы:

table = obj {
    nam = _"стол";
    Take = "Стол стоит там, где ему место.";
    alias [[Take: Push, Pull]];
}

Как видим, синтаксис очевиден. Push и Pull становятся синонимами Take.

Однако, иногда требуется не просто синоним, а перенаправление. Что значит перенаправление? Допустим, мы написали обработчик Exam (осмотреть) для устройств на столе. При попытке обыскать стол, мы хотим, чтобы вызвался обработчик Exam у устройств. Как это сделать?

devices = obj {
    nam = _'устройства';
    Exam = [[ На столе установлено множество различных непонятных устройств.]];
    Take = "Лучше их не трогать, а то еще сломаешь.";
    alias [[ Take: Pull, Push ]];
}
 
table = obj {
    nam = _'стол';
    Exam = [[Стол из тёмного металла, который поддерживает разнообразные устройства.]];
    Take = [[Стол намертво вделан в пол лаборатории.]];
    alias [[ Take: Pull, Push ]];
    Search = function(s)
            parser:redirect {'Exam', devices};
            return true -- вернуть true, чтобы прервать выполнение цепочки
    end;
    obj = { 'devices' };
}

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

            parser:redirect ( {'Give', apple, goblin }, {'Receive', goblin, apple});

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

    Search = redirect ('devices', 'Exam');

Этот пример сконструирует функцию Search, которая будет вызывать метод Exam у devices, без перезапуска цепочки событий.

Атрибуты

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

apple = obj {
        var { takeable = true };
        nam = _'яблоко';        
}

Однако, по сравнению с классическими играми INSTEAD, при написании парсерной игры, атрибуты используются гораздо чаще, чтобы сделать написание игры более удобным, введены конструкции attr и Attribute. Например, приведенный ранее пример можно упростить:

apple = obj {
        attr [[takeable]];
        nam = _'яблоко';        
}

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

apple = obj {
        attr [[takeable,dropable]];
        nam = _'яблоко';        
}

В данном примере, у apple будут два свойства: takeable и dropable и оба они будут установлены в true.

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

Attribute [[~examined]];

Символ ~ в начале атрибута означает отрицание. То есть, теперь, все объекты нашей игры при создании будут иметь атрибут examined равный false. Конечно, вы можете задавать несколько свойств, как и в конструкции attr. И вы можете комбинировать true и false атрибуты так, как вам угодно:

table = obj {
        attr [[openable,~opened];
        nam = _'стол';        
}

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

В стандартной библиотеке stdparser.lua определены некоторые атрибуты. Ниже представлена таблица с их описанием.

Атрибут Смысл
takeable объект можно брать
dropable объект можно выбросить
door дверь – будет описано позднее
openable можно открывать
opened открыт
removable можно извлечь
container в контейнер можно помещать другие объекты
eatable можно есть
male, female, plural, singualr м./ж. род, мн./ед. число, обычно не требуется

Механизм работы стандартных атрибутов поясним на примере takeable. В stdparser.lua реализована следующая функция:

parser.events.Take = function(self, s)
    if not s.takeable then -- пытаемся взять незабираемый объект
        if s:G().live then -- живой объект?
            live_act(self, s) -- сообщение вида -- ему это не понравится!
            return.
        end
        if person() == 1 then -- стандартные сообщения от 1/2/3 лица
            p ("Мне не ", parser:need(s), " ", s:M 'им', ".")
        elseif person() == 2 then
            p ("Тебе не ", parser:need(s), " ", s:M 'им', ".")
        else
            p (self:Me 'дт', " не ", parser:need(s), " ", s:M 'им', ".")
        end
        return
    end
    take(s) -- если объект takeable -- взять его
end;

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

Игрок

В недрах модуля создается объект игрок pl.

pl = parser_player {
};

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

При этом, вы можете заполнить атрибуты:

  • nam, по умолчанию я (актуально, если игра настроена для именования в 3-м лице);
  • person, по умолчанию 1 (1..3: от какого лица ведется игра);
  • live, по умолчанию true (одушевленность)

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

Словарь

Как мы уже выяснили, основой метапарсера являются глаголы. Но есть и второй важный компонент – это словарь и внутренний модуль для работы с ним (morpher).

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

толкнуть Габриэллу

игра (без всяких дополнительных усилий со стороны автора игры) отвечает: «Габриэлле это не понравится.», вместо сухого «Это нельзя сделать.».

Итак, работа с morpher понадобится вам в следующих случаях:

  • при отсутствии какого-либо слова в словаре;
  • при написании своих глаголов, если вы хотите сделать вывод на непрописанные события более осмысленными.

Поиск слов в словаре

Как уже было кратко сказано в главе Глаголы, при нажатии на f11 генерируется локальный словарь игры, в который опадают строки, начинающиеся с символа _ (Например: nam = _'яблоко'). Если в полном словаре слово не найдено, то словарь игры не будет содержать необходимые словоформы и парсер не сможет корректно склонять описание предмета.

В этом случае, вы можете сами добавить необходимое слово в словарь вашей игры. Например, написав в вашей игре (в глобальном пространстве, в init() или start()):

morph:word { live = true,
    female = true,
    singular = true,.
    "Габриэла",.
    "Габриэлу",.
    "Габриэле",.
    "Габриэлу",.
    "Габриэлой",.
    "Габриэле" }

Мы добавили все формы имени Габриэлла (именительный, родительный, дательный, винительный, творительный, предложный падежи). При необходимости, дополнительной строкой мы могли бы добавить еще местный падеж пр2. При этом, в качестве свойств можно задавать:

  • live - одушевл. (по умолчанию неод.);
  • neuter - ср. род;
  • male - м. род (по умолчанию);
  • female - ж. род;
  • plural - мн. число;
  • singular - ед. число (по умолчанию);

Задание необходимых свойств сделает информацию о слове полнее, что даст возможность корректно работать стандартной библиотеке.

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

girl = obj {
    nam = _'Габриэлла';
    live = true;
    female = true;
    singular = true;
    morph = { "Габриэла", "Габриэлу", "Габриэле", "Габриэлу", "Габриэлой", "Габриэле" };
}

На самом деле, вы можете прописывать признаки объекта live/female/male/singular/plural и другие даже в том случае, если вы не описываете падежные формы с помощью morph =. Это требуется в тех случаях, когда словарь выбирает не то слово, что требуется в игре или, например, вы хотите одушевить какой-то предмет. В этом смысле, чаще всего требуется задавать свойство live, хотя для очевидных слов (например, девушка) модуль morpher «угадывает» одушевленность сам.

В случае, если в nam или disp задано несколько синонимов, то варианты коррекции словаря можно записывать так:

ufo = obj {
    nam = _'объект|~нло|~штукенция';
    morph = { ["нло"] = { "НЛО" }, ["штукенция"] = { "штукенция", "штукенцию", "штукенции" ...
...

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

Следует также отметить, что в случае составных имен объекта (например: «летающая тарелка» или «Баба-Яга») при необходимости задания своего morph необходимо задавать компоненты по отдельности, так же как и в случае синонимов:

ufo = obj {
    nam = _'летающая тарелка';
    morph = { ["летающая"] = { "летающая", ".... "}, ["тарелка"] = { "тарелка", "... "} };
...

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

    morph = { ["баба"] = { "баба", ".... "}, ["Яга"] = { "Яга", "... "} };
...

Генерация падежных форм

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

Как это сделать?

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

Итак, для генерации падежей воспользуйтесь методом объекта M.

Например, вот реализация глагола двигать:

parser.events.after_Push = function(self, s)
    if self:react() then -- если реакция уже была, ничего не делаем
        return 
    end
    p ([[Нет смысла двигать ]], s:M 'вн', '.')
end

s - это объект, на которое направлено действие. s:M() – функция, которая генерирует падеж. Если параметр не указан – формируется слово в именительном падеже. В нашем случае, s:M 'вн' (синоним s:M('вн')) формирует слово в винительном падеже. Таким образом, s:M 'вн' для s, соответствующему объекту 'полка' вернет 'полку'.

Иногда, нужно вернуть слово с заглавной буквы (например, в начале предложения), для этого воспользуйтесь методом CM:

parser.events.after_Push = function(self, s)
    if self:react() then -- если реакция уже была, ничего не делаем
        return 
    end
    p (s:CM()..[[ стоит там, где нужно.]])
end

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

parser.events.after_Push = function(self, s)
    if self:react() then -- если реакция уже была, ничего не делаем
        return 
    end
    p (s:CM(), [[ стоит там, где ]], s:it 'дт', [[нужно.]])
end

Используйте It для формирования местоимения с большой буквы.

При именовании главного героя в третьем лице, удобно воспользоваться методами parser:me()/parser:Me(), которые раскроют nam главного героя в нужном падеже:

p(parser:Me 'вн',  ' хочется есть');

Еще одна вспомогательная функция: parser:need(), которая формирует слово 'нужно', 'нужна', 'нужны', 'нужен' в зависимости от рода и числа.

Анализ

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

Например, реализация события Take:

parser.events.Take = function(self, s)
    if not s.takeable then
        if s:G().live then
            p (s:CM 'дт', " это не понравится.")
            return.
        end
        p ("Мне не ", parser:need(s), " ", s:M 'им', ".")
        return
    end
    take(s)
end;

Метод G() объекта возвращает таблицу, в которой заполнены все свойства: live/singular/plural/neuter и т.д.

Стандартная библиотека

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

Глаголы

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

Набор глаголов библиотеки является глобальным, и расположен в таблице game.verbs. Каждый глагол добавляется туда во время загрузки библиотеки с помощью функций parser.verb.Имя_группы_глаголов(). Таким образом, чтобы контролировать набор глаголов, вы можете в вашей игре написать:

game.verbs = {}

А затем, вызвать те функции parser.verb.XXXXX, которые добавят в вашу игру необходимые глаголы.

Ниже приведен список глаголов, и соответствующие им функции и события.

Глагол Событие Группа
инвентарь Inventory parser.verb.Inventory()
осмотреть, изучить, исследовать Exam (объект инвентаря или сцены или игрок) parser.verb.Exam()
идти, зайти, залезть, лезть, забираться, подойти, пойти Walk, WalkIn, Climb parser.verb.Walk()
посмотреть, смотреть LookAt parser.verb.LookAt()
искать, обыскать Search/SearchOn/SearchUnder parser.verb.Search()
открыть Open → OpenBy parser.verb.Open()
закрыть Close parser.verb.Close()
толкнуть, двигать, сдвинуть Push parser.verb.Push()
тянуть Pull parser.verb.Pull()
нюхать, понюхать Smell parser.verb.Smell()
есть, съесть Eat parser.verb.Eatl()
поставить, положить, вставить, воткнуть PutOn→ReceiveOn, PutIn→Receive, PutUnder→ReceiveUnder parser.verb.Put()
есть, съесть Eat parser.verb.Eatl()
ударить, бить, разбить, сломать Attack parser.verb.Attack()
взять, забрать Take parser.verb.Take()
бросить, выбросить, кинуть Drop → Receive, ReceiveOn parser.verb.Drop()
извлечь, вытащить Remove parser.verb.Remove()
ждать, подождать Wait parser.verb.Wait()
читать, почитать, прочитать Read parser.verb.Read()
привязать Tie parser.verb.Tie()
говорить, поговорить TalkTo parser.verb.TalkTo()
отдать, дать, подарить Give→Receive parser.verb.Give()

Таким образом, в самом модуле эти группы глаголов инициализируются следующим образом:

game.verbs = {};
 
parser.verb.Inventory()
parser.verb.Exam()
parser.verb.Walk()
parser.verb.LookAt()
parser.verb.Search()
parser.verb.Open()
parser.verb.Close()
parser.verb.Push()
parser.verb.Pull()
parser.verb.Smell()
parser.verb.Eat()
parser.verb.Put()
parser.verb.Attack()
parser.verb.Take()
parser.verb.Drop()
parser.verb.Remove()
parser.verb.Wait()
parser.verb.Read()
parser.verb.Tie()
parser.verb.TalkTo()
parser.verb.Give()

В своей игре вы можете сократить набор глаголов, заменить какие-то реализации и т.д. Вы можете передавать параметр в parser.verb…, если хотите добавить глагол в конкретную комнату или игрока. Внимание! Все эти функции можно использовать только в глобальном контексте, init() или start(). Например:

game.verbs = {};
 
parser.verb.Inventory()
parser.verb.Exam()
parser.verb.Walk()
parser.verb.Search()
parser.verb.Open()
parser.verb.Smell()
parser.verb.Put()
parser.verb.Take()
parser.verb.Drop()
parser.verb.Wait()
parser.verb.Read()
parser.verb.Tie()
parser.verb.TalkTo()
parser.verb.Give()
Verb { "Help", "~помощь|~инфо" }
Verb { "Sit %3", "сесть", "в|на", "{obj}вн" };
Verb { "Stand", "встать" };
Verb { "Say %2", "сказать|~произнести", "{text}" };
Verb { "Kiss %2", "~поцеловать|~целовать", "{obj}вн" };
Verb { "Take %2", "~сорвать", "{obj}вн" };

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

parser.verb._Service()

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

События

Выше были перечислены глаголы и события, которые они порождают. Рассмотрим события подробнее. Ниже приведена таблица параметров и обработчиков parser.events…. В угловых скобках указаны необязательные параметры. что, куда и подобное – означает объект. редирект это перенаправление события (redirect).

Событие Параметры parser.events.before_Событие .. .Событие .. .after_Событие
Inventory нет нет Показывает инвентарь игрока нет
Exam [что] Если параметров нет, показать всю сцену нет сообщение о том, что ничего необычного не обнаружено
Walk комната, объект или сторона (в текст. виде) нет реализует переход нет
WalkIn объект нет реализует переход (внутрь объекта, в двери) нет
Climb объект нет сообщение о невозможности взбираться нет
LookAt что нет нет перенаправление на Exam
Search, SearchOn, SearchUnder объект инвентаря или сцены нет для одушевленных - реакция сообщение о том, что ничего не обнаружено
Open что, [чем] нет проверка признаков openable и opened, открытие сообщение об успешном открытии
OpenBy [чем], что нет нет нет
Close что, [чем] нет проверка признаков openable и opened, закрытие сообщение об успешном закрытии
Push что, [к чему] нет для одушевленных - реакция сообщение о том, что нет смысла это делать
Pull что нет для одушевленных - реакция сообщение о том, что нет смысла это делать
Moved к чему, [что] нет нет нет
Smell что нет нет никакого необычного запаха нет
Eat что (инвентарь) нет проверка признаков live и eatable, еда сообщение о том, что игрок съел предмет
PutIn, PutOn, PutUnder что, куда нет проверка dropable, container, openable и opened сообщение об успешном перемещении предмета
Receive кому, что
Attack что, [чем] нет нет «агрессия не оправдана»
Take что (объект сцены) нет проверка takeable, live. забрать объект сообщение о том, что предмет забран
Drop что, [куда] нет проверка dropable, реализация выкидывания сообщение об успешном выкидывании объекта
Receive, ReceiveOn куда, что нет нет нет
Remove что, [откуда] нет проверка takeable с возможным редиректом на Take, проверка removable сообщение об успешном извлечении
Wait нет нет Сообщение о том, что прошло немного времени нет
Read что нет редирект на Exam нет
Tie что к чему нет Информация о том, что в этом нет смысла нет
TalkTo с чем нет Вас игнорируют нет
Give что, чему нет Сообщение о неудачном вручении нет

cutscene

В начале этого руководства мы кратко упомянули сцену типа cutscene. Через нее удобно реализовывать переходные сцены. cutscene реализована как комната с единственным глаголом «Дальше» (Next), который перебрасывает игрока в комнату, заданную через свойство walk_to. При этом, dsc комнаты может содержать список строк, который будет выводится постепенно, по мере выбора игроком «дальше», например:

main = cutscene {
    nam = 'Введение';
    dsc = {
       "1",
       "2",
       "3",
       "4",
       "5",
       "Вышел зайчик погулять!",
    };
    walk_to = 'room1';
}

Компас

В метапарсере для перехода между сценами можно использовать несколько механизмов:

  1. стороны света;
  2. двери;
  3. явная реализация события Walk у объекта;

Сейчас поговорим о сторонах света. Традиционно, в парсерных играх можно написать:

идти на юг

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

lab = room {
    nam = 'Лаборатория';
    n_to = 'storage';
    e_to = 'kitchen';
}

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

Атрибут Возможность ввести фразу
n_to идти на север, север
e_to идти на восток, восток
s_to идти на юг, юг
w_to идти на запад, запад
u_to идти наверх, наверх
d_to идти вниз, вниз

Кроме этого, есть диагональные направления, например: se_to ⇒ северо-восток.

По умолчанию, метапарсер будет активировать глаголы при наличии соответствующих атрибутов у комнаты, но вы можете форсировать постоянное наличие этих глаголов (важно для классических парсерных игр) с помощью задания:

parser.compass = true

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

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

parser.directions = { 
	{"n_to", "на", "север" };
	{"e_to", "на", "восток"};
	{"s_to", "на", "юг"};
	{"w_to", "на", "запад"};
	{"d_to", "вниз"};
	{"u_to", "наверх"};
	{"u_to", "вверх"};
	{"r_to", "направо" };
	{"l_to", "налево" };
	{"f_to", "прямо" };
}

Но это нужно делать до вызова parser:init() или определения своих глаголов.

Двери

Дверь – это объект, в который можно зайти. У двери должен быть выставлен атрибут door. Дверь может быть openable объектом, и иметь состояния закрыто/открыто. При этом, войти в закрытую дверь нельзя. Вы можете задать атрибут when_closed, который будет выведен вместо стандартного сообщения при попытке зайти в закрытую дверь.

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

grotdoor = obj {
    nam = _"дверь|~металлическая дверь";
    attr [[ openable,door ]];
    when_closed = "Металлическая дверь закрыта.";
    door_to = function(s)
        if here() == grot2 then
            return 'grot3'
        else
            return 'grot2'
        end
    end;
    after_Open = "Ты прикладываешь усилие и открываешь дверь.";
    after_Close ="Ты с усилием закрываешь металлическую дверь."
}

Темные комнаты

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

droom = darkroom {
	nam = 'Кладовая';
	dsc = [[При свете лампы вы разглядели, что это -- кладовая.]];
	w_to = 'pogreb';
	obj = {'lopata'};
}

По-умолчанию комната создается темной, в дальнейшем вы можете включить и выключить свет:

droom:light(true) -- включить свет
droom:light(false) -- выключить свет
if droom:light() then ... -- если горит свет

Диалоги

Диалоги могут быть реализованы несколькими способами:

  1. Использовать только глагол поговорить с (TalkTo), без возможности задания темы разговора;
  2. Использовать стандартные диалоги INSTEAD, которые в метапарсере отобразятся в некий полу-парсерный вариант, когда игрок может задавать темы вопросов в поле ввода;
  3. Использовать свой глагол поговорить в которой тема задается подстановкой {text}, и затем анализировать совпадение с желаемой темой разговора функцией find;
  4. Использовать свой глагол поговорить в котором параметр темы задается функцией, которая возвращает перечисление тем в виде строк;

Для вашей первой игры я рекомендую 1-й способ.

Функции

Ниже перечислены некоторые полезные функции.

parser:cls() – очистить вывод;

parser:curevent() – вернуть имя текущего события ('Take', 'TalkTo' и т.д.)

Функция instead.get_title – реализация статусной строки сверху над игровым окном, возвращает строку или пустоту;

Для отключения статусной строки, вы можете задать:

parser.notitle = true

Функция game.fading – реализована таким образом, что запрещает эффект перехода при включенном режиме scroll. Вы можете переписать эту функцию так, как вам это нравится, например:

game.fading = function(s)
    if player_moved() then
        return game.gui.fading
    end
    return false
end

Графика

Конечно, вы можете вставлять графику с помощью img и с помощью задания атрибута pic у комнаты и/или у game (как это описано в документации INSTEAD). Но в этом случае графика будет постоянно выводиться, что не всегда желательно в случае парсерной игры. Для такой игры лучше использовать атрибут gfx у комнаты. Примерно так, как используется pic. Например:

home = room {
    nam = 'милый дом';
    gfx = 'gfx/home.png';
    dsc = [[Я снова дома.]];
}

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

Навигация
Печать, экспорт
Инструменты
Язык
Перевод этой страницы:
Инструменты
Ссылки