Содержание

Базовая документация

Общие сведения

Код игр для STEAD пишется на lua (5.1), поэтому, знание этого языка полезно, хотя и не необходимо. Код движка на lua занимает около ~3000 строк и лучшей документацией является изучение его кода.

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

Статическая часть сцены отображается только один раз, при показе сцены, или при повторении команды look (в графическом интерпретаторе – клик на названии сцены). Динамическая часть сцены составлена из описаний объектов сцены, она отображается всегда.

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

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

Действиями игрока могут быть:

Игра представляет из себя каталог, в котором должен находиться скрипт main.lua. Другие ресурсы игры (скрипты на lua, графика и музыка) должны находиться в рамках этого каталога. Все ссылки на ресурсы делаются относительно текущего каталога – каталога игры.

В начале файла main.lua может быть определен заголовок, состоящий из тегов. Теги должны начинаться с символов : комментарий с точки зрения lua. На данный момент существует один тег: $Name:, который должен содержать название игры в кодировке UTF-8. Пример использования тега:

-- $Name: Самая интересная игра!$

Сразу после заголовков вам необходимо указать версию STEAD API, которая требуется игре. На данный момент последняя версия 1.4.5.

instead_version "1.4.5"

Важно!

Если version отсутствует, то STEAD API будет работать в режиме совместимости (устаревшее API).

Инициализацию игры следует описывать в функции init:

function init()
    me()._know_truth = false
    take(knife);
    take(paper);
end

Графический интерпретатор ищет доступные игры в каталоге games. Unix-версия интерпретатора кроме этого каталога просматривает также игры в каталоге ~/.instead/games. Windows-версия: Documents and Settings/USER/Local Settings/Application Data/instead/games. В Windows- и standalone-Unix-версии игры ищутся в каталоге ./appdata/games, если он существует.

1. Сцена

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

main = room {
	nam = 'главная комната',
	dsc = 'Вы в большой комнате.',
};

Запись означает создание объекта main типа room. У каждого объекта игры есть атрибуты и обработчики. Например, атрибут nam (имя) является необходимым для любого объекта.

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

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

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

main = room {
	nam = 'главная комната';
	dsc = 'Вы в большой комнате.';
};

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

game.forcedsc = true;

Или, аналогично, задать атрибут forcedsc для конкретных сцен.

Для длинных описаний удобно использовать запись вида:

dsc = [[ Очень длинное описание... ]],

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

dsc = [[ Первый абзац. ^^
Второй Абзац.^^
 
Третий абзац.^
На новой строке.]],

2. Объекты

Объекты – это единицы сцены, с которыми взаимодействует игрок.

tabl = obj {
	nam = 'стол',
	dsc = 'В комнате стоит {стол}.',
	act = 'Гм... Просто стол...',
};

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

dsc – описание объекта. Оно будет выведено в динамической части сцены. Фигурными скобками отображается фрагмент текста, который будет являться ссылкой в графическом интерпретаторе.

act – это обработчик, который вызывается при действии пользователя (действие на объект сцены). Его задача – возвращение строки текста, которая станет частью событий сцены, или логического значения (см. раздел 5).

ВНИМАНИЕ: в пространстве имен lua уже существуют некоторые объекты (таблицы), например: table, io, string… Будьте внимательны при создании объекта. Например, в приведенном примере используется tabl, а не table. Лучше не использовать подобные названия, хотя в новых версиях INSTEAD эта проблема во многом решена.

3. Добавляем объекты в сцену

Ссылкой на объект называется текстовая строка, содержащая имя объекта при его создании. Например: 'tabl' – ссылка на объект tabl.

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

main = room {
	nam = 'главная комната',
	dsc = 'Вы в большой комнате.',
	obj = { 'tabl' },
};

Теперь, при отображении сцены мы увидим объект «стол» в динамической части.

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

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

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

apple = obj {
	nam = 'яблоко',
	dsc = 'На столе лежит {яблоко}.',
	act = 'Взять что-ли?',
};
 
tabl = obj {
	nam = 'стол',
	dsc = 'В комнате стоит {стол}.',
	act = 'Гм... Просто стол...',
	obj = { 'apple' },
};

При этом, в описании сцены мы увидим описание объектов стол и яблоко, так как apple – связанный с tabl объект.

5. Атрибуты и обработчики как функции

Большинство атрибутов и обработчиков могут быть функциями. Так, например:

nam = function()
	return 'яблоко';
end,

Это синоним записи: nam = 'яблоко';

Обработчик должен вернуть строку. Если вам удобнее, для возвращения текста вы можете использовать функции:

Если p/pn/pr вызывается с одним текстовым параметром, то скобки можно опускать.

Используйте .. или , для склейки строк. Например:

pn "Нет скобкам!";
pn ("Строка 1".." Строка 2");
pn ("Строка 1", "Строка 2");

Функции сильно расширяют возможности STEAD, например:

apple = obj {
	nam = 'яблоко',
	dsc = function(s)
		if not s._seen then
			p 'На столе {что-то} лежит.';
		else
			p 'На столе лежит {яблоко}.';
		end
	end,
	act = function(s)
		if s._seen then
			p 'Это яблоко!';
		else
			s._seen = true;
			p 'Гм... Это же яблоко!';
		end
	end,
};

Если атрибут или обработчик оформлен как функция, то обычно первый аргумент функции (s) – сам объект.

В данном примере при показе сцены в динамической части сцены будет выведен текст: 'На столе что-то лежит'. При взаимодействии с 'что-то', переменная _seen объекта apple будет установлена в true и мы увидим, что это было яблоко.

Запись s._seen означает, что переменная _seen размещена в объекте s (то-есть apple). Подчеркивание означает, что эта переменная попадет в файл сохранения игры.

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

global {
    global_var = 1;    
}
main = room {
    var {
        i = "a";
        z = "b";
    };
    nam = 'Моя первая комната';
    var {
        new_var = 3;
    };
    dsc = function(s)
        p ("i == ", s.i);
        p ("new_var == ", s.new_var);
        p ("global_var == ", global_var);
    end;

Переменные записываются в файл сохранения, если они размещены в одном из перечисленных типов объектов: комната, объект, игра, игрок, глобальное пространство, при этом начинаются с символа _ или определены с помощью var и global.

Начиная с версии 1.2.0 вы можете определять функции следующим образом:

	dsc = code [[
		if not self._seen then
			p 'На столе {что-то} лежит.';
		else
			p 'На столе лежит {яблоко}.';
		end
	]],

При этом в self записан текущий объект, arg1 … arg9 и массив args[] – параметры.

В файл сохранения могут быть записаны:

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

button = obj {
	nam = "кнопка",
	dsc = "На стене комнаты видна большая красная {кнопка}.",
	act = function (s)
         	here()._dynamic_dsc = [[После того как я нажал на кнопку, комната преобразилась. 
		  Книжный шкаф куда-то исчез вместе со столом и комодом, а на его месте 
		  появился странного вида аппарат.]];
	        return true;
        end,
}
r12 = room {
	nam = 'комната',
	_dynamic_dsc = 'Я нахожусь в комнате.',
	dsc = function (s) return s._dynamic_dsc end,
	obj = {'button'}
}

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

Если необходимо показать, что действие невыполнимо, ничего не возвращайте. При этом будет отображено описание по умолчанию, заданное с помощью обработчика game.act. Обычно описание по умолчанию содержит описание невыполнимых действий.

Обратите внимание, что для создания динамического описания сцены в рассмотренном выше примере использовалось новая переменная _dynamic_dsc. Это сделано для того, чтобы изменённое описание попало в файл сохранения игры. Поскольку имя dsc не начинается с подчёркивания или заглавной буквы, по умолчанию оно не попадёт в файл сохранения.

Данный пример мог бы выглядеть более разумно:

button = obj {
	nam = "кнопка";
	dsc = "На стене комнаты видна большая красная {кнопка}.";
	act = function (s)
         	here().dsc = [[Теперь комната выглядит совсем по-другому!!!]];
	        pn [[После того как я нажал на кнопку, комната преобразилась. 
		  Книжный шкаф куда-то исчез вместе со столом и комодом, а на его месте 
		  появился странного вида аппарат.]];
        end,
}
 
r12 = room {
	nam = 'комната';
        forcedsc = true;
        var {
	        dsc = 'Я нахожусь в комнате.';
        };
	obj = {'button'}
}

6. Инвентарь

Простейший вариант сделать объект, который можно брать – определить обработчик tak.

Например:

apple = obj {
	nam = 'яблоко',
	dsc = 'На столе лежит {яблоко}.',
	inv = function(s)
		inv():del(s);
		return 'Я съел яблоко.';
	end,
	tak = 'Вы взяли яблоко.',
};

При этом, при действии игрока на объект «яблоко» – яблоко будет убрано из сцены и добавлено в инвентарь. При действии игрока на инвентарь – вызывается обработчик inv.

В нашем примере, при действии игроком на яблоко в инвентаре – яблоко будет съедено.

7. Переходы между сценами

Для перехода между сценами используется атрибут сцены – список way.

room2 = room {
	nam = 'зал',
	dsc = 'Вы в огромном зале.',
	way = { 'main' },
};
 
 
main = room {
	nam = 'главная комната',
	dsc = 'Вы в большой комнате.',
	obj = { 'tabl' },
	way = { 'room2' },
};

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

При переходе между сценами движок вызывает обработчик exit из текущей сцены и enter в той сцены, куда идет игрок. Например:

room2 = room {
	enter = 'Вы заходите в зал.',
	nam = 'зал',
	dsc = 'Вы в огромном зале.',
	way = { 'main' },
	exit = 'Вы выходите из зала.',
};

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

room2 = room {
	enter = function(s, f)
		if f == main then
			return 'Вы пришли из комнаты.';
		end
	end,
	nam = 'зал',
	dsc = 'Вы в огромном зале.',
	way = { 'main' },
	exit = function(s, t)
		if t == main then
			return 'Я не хочу назад!', false
		end
	end,
};

Как видим, обработчики могут возвращать два значения: строку и статус. В нашем примере функция exit вернет false, если игрок попытается уйти из зала в комнату main. false означает, что переход не будет выполнен. Такая же логика работает и для enter. Кроме того, она работает и для обработчика tak.

Если вы используете функции p/pn/pr, то просто возвращайте статус операции с помощью return:

room2 = room {
	enter = function(s, f)
		if f == main then
			p 'Вы пришли из комнаты.';
		end
	end,
	nam = 'зал',
	dsc = 'Вы в огромном зале.',
	way = { 'main' },
	exit = function(s, t)
		if t == main then
			p 'Я не хочу назад!'
                        return false
		end
	end,
};

Следует отметить, что при вызове обработчика enter текущая сцена может быть еще не изменена!!! Начиная с версии 1.2.0 добавлены обработчики left и entered, которые вызываются после того, как переход произошел. Эти обработчики рекомендованы к использованию всегда, когда нет необходимости запрещать переход.

8. Действие объектов друг на друга

Игрок может действовать объектом инвентаря на другие объекты. При этом вызывается обработчик use у объекта, которым действуют, и used – на который действуют.

Например:

knife = obj {
	nam = 'нож',
	dsc = 'На столе лежит {нож}',
	inv = 'Острый!',
	tak = 'Я взял нож!',
	use = 'Вы пытаетесь использовать нож.',
};
 
tabl = obj {
	nam = 'стол',
	dsc = 'В комнате стоит {стол}.',
	act = 'Гм... Просто стол...',
	obj = { 'apple', 'knife' },
	used = 'Вы пытаетесь сделать что-то со столом...',
};

Если игрок возьмет нож и использует его на стол – то он увидит текст обработчиков use и used. use и used могут быть функциями. Тогда первый параметр это сам объект, а второй – объект на который направлено действие в случае use и объект, которым действие осуществляется в случае used.

use может вернуть статус false, в этом случае обработчик used не вызовется (если он вообще был). Статус обработчика used игнорируется.

Пример:

knife = obj {
	nam = 'нож',
	dsc = 'На столе лежит {нож}',
	inv = 'Острый!',
	tak = 'Я взял нож!',
	use = function(s, w)
		if w ~= tabl then
			p 'Не хочу это резать.'
                        return false
		else
			p 'Вы вырезаете на столе свои инициалы.';
		end
	end
};

В примере выше нож можно использовать только на стол.

9. Объект "игрок"

Игрок в STEAD представлен объектом pl. Тип объекта – player. В движке объект создается следующим образом:

pl = player {
	nam = "Incognito",
	where = 'main',
	obj = { }
};

Атрибут obj представляет собой инвентарь игрока.

10. Объект ''game''

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

game = game {
	nam = "INSTEAD -- Simple Text Adventure interpreter v"..version.." '2009 by Peter Kosyh",
	dsc = [[
Commands:^
    look(or just enter), act <on what> (or just what), use <what> [on what], go <where>,^
    back, inv, way, obj, quit, save <fname>, load <fname>.]],
	pl ='pl',
	showlast = true,
};

Как видим, объект хранит в себе указатель на текущего игрока (pl) и некоторые параметры. Например, вы можете указать в начале своей игры кодировку текста следующим образом:

game.codepage="UTF-8"; 

Поддержка произвольных кодировок изначально присутствует в UNIX версии интерпретатора, в windows версии – начиная с 0.7.7.

Кроме того, объект game может содержать обработчики по умолчанию act, inv, use, которые будут вызваны, если в результате действий пользователя не будут найдены никакие другие обработчики. Например, вы можете написать в начале игры:

game.act = 'Не получается.';
game.inv = 'Гм.. Странная штука..';
game.use = 'Не сработает...';

11. Атрибуты-списки

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

Методы списков: add, del, look, srch, purge, replace. Из них наиболее часто используемые: add и del.

Следует отметить, что параметром add, del, purge, replace и srch может быть не только сам объект или идентификатор объекта, но и имя объекта.

Начиная с версии 0.8 параметром add может быть сам объект. Кроме того, с этой версии добавляется необязательный второй параметр – позиция в списке. Начиная с версии 0.8 вы можете также выполнять модификацию списка по индексу с помощью метода set. Например:

objs():set('knife',1);

Выше, вы уже видели пример со съеденным яблоком, там использовалась конструкция inv():del('apple');

inv() – это функция, которая возвращает список инвентаря. del после : – метод, удаляющий элемент инвентаря.

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

knife = obj {
	nam = 'нож',
	dsc = 'На столе лежит {нож}',
	inv = 'Острый!',
	act = function(s)
		objs():del(s);
		inv():add(s);
	end,
};

Кроме добавления, удаления объектов из списков вы можете использовать выключение/включение объектов с помощью методов enable() и disable(). Например: knife:disable(). При этом объект knife пропадает из описания сцены, но в последствии может быть опять быть включен, с помощью knife:enable().

Начиная с версии 0.9.1 доступны методы zap() и cat(). zap() – обнуляет список. cat(b, [pos]) – добавляет в список содержимое b на позицию pos.

Начиная с версии 0.9.1 доступны методы disable_all() и enable_all(), выключающие и включающие вложенные в объект объекты.

Внимание!!! На данный момент для работы с инвентарем и объектами рекомендуется использовать более высокоуровневые функции: put/get/take/drop/remove/seen/have и др.

12. Функции, которые возвращают объекты

В STEAD определены некоторые функции, которые возвращают наиболее часто используемые объекты. Например:

Комбинируя эти функции с методами add, del можно динамически менять сцену, например:

ways():add('nextroom'); -- добавить переход на новую сцену;
objs():add('chair'); -- добавить объект в текущую сцену;

Еще одна функция, которая получает объект по ссылке: ref().

Например, мы можем добавить объект в локацию home следующим образом:

ref('home').obj:add('chair');

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

home.obj:add('chair');

Или, для версии >=0.8.5:

objs('home'):add('chair');

или, наконец:

put('chair', 'home');

или даже:

put(chair, home);

Начиная с 0.8.5 – deref(o), возвращает ссылку-строку для объекта;

13. Некоторые вспомогательные функции.

В STEAD определены некоторые высокоуровневые функции, которые могут оказаться полезными при написании игры.

...
act = function(s)
	if have('knife') then
		return 'Но у меня же есть нож!';
	end
end
...

Следующие варианты тоже будут работать:

...
	if have 'knife' then
...
	if have (knife) then
...

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

move('mycat','inmycar');

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

Вы можете указать исходную позицию (комнату) объекта в качестве третьего параметра move.

move('mycat','inmycar', 'forest'); 

Начиная с версии 0.8 присутствует также функция movef, аналогичная move, но добавляющая объект в начало списка.

if seen('mycat') then
	move('mycat','inmycar');
end

Начиная с 0.8.6 – необязательный второй параметр – сцена.

drop('knife');

Начиная с версии 0.8 присутствует также функция dropf, аналогичная drop, но добавляющая объект в начало списка. Начиная с версии 0.8.5 второй необязательный параметр – комната, куда помещается предмет. Кроме того, для версий >=0.8.5 доступны похожие функции put/putf, которые не удаляют предмет из инвентаря.

Начиная с 0.8.9 – присутствует функция remove(o, [from]), удаляет объект из текущей сцены или сцены from.

	take('knife');

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

act = code [[
        pn "Я иду в следующую комнату..."
        goto (nextroom);
]]
...
act = code [[
        return cat('Я иду в следующую комнату', goto (nextroom));
]]

Внимание!!!

После вызова goto выполнение обработчика продолжится до его завершения.

mycar = obj {
	nam = 'моя машина',
	dsc = 'Перед хижиной стоит мой старенький {пикап} Toyota.',
	act = function(s)
		return goto('inmycar');
	end
};

14. Диалоги

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

povardlg = dlg {
	nam = 'на кухне',
	dsc = 'Передо мной полное лицо женщины - повара в белом колпаке и усталым взглядом...',
	obj = {
	[1] = phr('Мне вот-этих зелененьких... Ага -- и бобов!', 'На здоровье!'),
	[2] = phr('Картошку с салом, пожалуйста!', 'Приятного аппетита!'),
	[3] = phr('Две порции чесночного супа!!!', 'Прекрасный выбор!'),
	[4] = phr('Мне что-нибудь легонькое, у меня язва...', 'Овсянка!'),
	},
};

phr – создание фразы. Фраза содержит вопрос, ответ и реакцию (реакция в данном примере отсутствует). Когда игрок выбирает одну из фраз, фраза отключается. Когда все фразы отключатся диалог заканчивается. Реакция – это строка кода на lua, который выполнится после отключения фразы. Например:

food = obj {
	nam = 'еда',
	inv = function (s)
		inv():del('food');
		return 'Я ем.';
	end
};
 
gotfood = function(w)
	inv():add('food');
	food._num = w;
	back();
end
 
povardlg = dlg {
	nam = 'на кухне',
	dsc = 'Передо мной полное лицо женщины - повара в белом колпаке и усталым взглядом...',
	obj = {
	[1] = phr('Мне вот-этих зелененьких... Ага -- и бобов!', 'На здоровье!', [[pon(); gotfood(1);]]),
	[2] = phr('Картошку с салом, пожалуйста!', 'Приятного аппетита!', [[pon(); gotfood(2);]]),
	[3] = phr('Две порции чесночного супа!!!', 'Прекрасный выбор!', [[pon();gotfood(3);]]),
	[4] = phr('Мне что-нибудь легонькое, у меня язва...', 'Овсянка!', [[pon(); gotfood(4);]]),
	},
};

В данном примере, игрок выбирает еду. Получает ее (запомнив выбор в переменной food._num) и возвращается обратно (в ту сцену откуда попал в диалог).

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

Если параметр n не указан, действие относится к текущей фразе.

Переход в диалог осуществляется как переход на сцену:

povar = obj {
	nam = 'повар',
	dsc = 'Я вижу {повара}.',
	act = function()
		return goto('povardlg');
	end,
};

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

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

facectrl = dlg {
	nam = 'фэйсконтроль',
	dsc = 'Я вижу перед собой неприятное лицо полного охранника.',
	obj = {
		[1] = phr('Я пришел послушать лекцию Белина...', 
		'-- Я не знаю кто вы -- ухмыляется охранник -- но мне велели пускать сюда только приличных людей.',
		[[pon(2);]]),
		[2] = _phr('У меня есть приглашение!', 
		'-- А мне плевать! Посмотри на себя в зеркало!!! Ты пришел слушать самого Белина -- правую руку самого... -- охранник почтительно помолчал -- Так что пошел вон..', [[pon(3,4)]]),
		[3] = _phr('Сейчас я дам тебе по роже!', '-- Ну все... Мощные руки выталкивают меня в коридор...',
			[[poff(4)]]),
		[4] = _phr('Ты, кабан! Я же тебе сказал -- у меня есть приглашение!',
			'-- Чтоооооо? Глаза охранника наливаются кровью... Мощный пинок отправляет меня в коридор...',
			[[poff(3)]]),
	},
	exit = function(s,w)
		s:pon(1);
	end,
};

_phr – создает выключенную фразу, которую можно включить. Данный пример показывает также возможность использования методов pon, poff, prem для диалога (см. exit).

Вы можете включать/выключать/удалять/проверять фразы не только текущего, но и произвольного диалога, с помощью методов объекта диалог pon/poff/prem/pseen/punseen. Например: shopman:pon(5);

15. Облегченные объекты

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

sside = room {
	nam = 'южная сторона',
	dsc = [[Я нахожусь у южной стены здания института. ]],
	act = function(s, w)
		if w == "подъезд" then
			ways():add('stolcorridor');
			p "Я подошел к подъезду. На двери подъезда надпись -- 'Столовая'. Хм -- зайти внутрь?";
		elseif w == "люди" then
			p 'Те, кто выходят, выглядят более довольными...';
		end
	end,
	obj = { vobj("подъезд", "У восточного угла находится небольшой {подъезд}."),
		vobj("люди", "Время от времени дверь подъезда хлопает впуская и выпуская {людей}.")},
};

Как видим, vobj позволяет сделать легкую версию статического объекта, с которым тем не менее можно взаимодействовать (за счет определения обработчика act в сцене и анализа имени объекта). vobj также вызывает метод used, при этом в качестве третьего параметра передается объект, воздействующий на виртуальный объект.

Синтаксис vobj: vobj(имя, описатель);

Существует модификация объекта vobjvway. vway реализует ссылку. Синтаксис vway: vway(имя, описатель, сцена назначения); например:

	obj = { vway("дальше", "Нажмите {здесь}.", 'nextroom') }

Вы можете динамически заполнять сцену объектами vobj или vway с помощью методов add и del. Например:

	objs(home):add(vway("next", "{Дальше}.", 'next_room'));
	-- здесь какой-нибудь код
	home.obj:del("next");

Определена также упрощенная сцена vroom. Синтаксис: vroom(имя перехода, сцена назначения). Например:

	home.obj:add(vroom("идти на запад", 'mountains');

16. Динамические события

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

mycat = obj {
	nam = 'Барсик',
	lf = {
		[1] = 'Барсик шевелится у меня за пазухой.',
		[2] = 'Барсик выглядывает из-за пазухи.',
		[3] = 'Барсик мурлычит у меня за пазухой.',
		[4] = 'Барсик дрожит у меня за пазухой.',
		[5] = 'Я чувствую тепло Барсика у себя за пазухой.',
		[6] = 'Барсик высовывает голову из-за пазухи и осматривает местность.',
	},
	life = function(s)
		local r = rnd(6);
		if r > 2 then
			return;
		end
		r = rnd(6);
		return s.lf[r];
	end,
....
 
profdlg2 = dlg {
	nam = 'Белин',
	dsc = 'Белин бледен. Он смотрит на дробовик рассеянным взглядом.',
	obj = {
		[1] = phr('Я пришел за своим котом.',
	'Я выхватываю Барсика из руки Белина и засовываю себе за пазуху.',
		[[inv():add('mycat'); lifeon('mycat')]]),
....

Любой объект или сцена могут иметь свой обработчик life, который вызывается каждый раз при смене текущего времени игры, если объект или сцена были добавлены в список живых объектов с помощью lifeon. Не забывайте удалять живые объекты из списка с помощью lifeoff, когда они больше не нужны. Это можно сделать, например, в обработчике exit, или любым другим способом.

life метод может возвращать текст события, который печатается после описания сцены.

Начиная с версии 0.9.1 вы можете вернуть из обработчика life второй код возврата, важность. (true или false). Например:

return 'В комнату вошел охранник.', true

Или:

    p 'В комнату вошел охранник.'
    return true

При этом текст события будет выведен до описания объектов.

17. Графика и музыка

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

home = room {
	pic = 'gfx/home.png',
	nam = 'дома',
	dsc = 'Я у себя дома',
};

Конечно, pic может быть функцией, расширяя возможности разработчика. Если в текущей сцене не определен атрибут pic, то берется атрибут game.pic. Если не определен и он, то картинка не отображается.

Начиная с версии 0.9.2 вы можете использовать в качестве картинок анимированные gif файлы.

Начиная с версии 0.9.2 вы можете встраивать графические изображения в текст или в инвентарь с помощью функции img. Например:

knife = obj {
	nam = 'Нож'..img('img/knife.png'),
}

А начиная с 1.3.0 поддерживается обтекание картинок текстом. Если картинка вставляется с помощью функции imgl/imgr, она будет расположена у левого/правого края. Такие картинки не могут быть ссылками.

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

imgl 'pad:16,picture.png' -- отступы по 16 от каждого края
imgl 'pad:0 16 16 4,picture.png' -- отступы: вверху 0, справа 16, внизу 16, слева 4
imgl 'pad:0 16,picture.png' -- отступы: вверху 0, справа 16, внизу 0, слева 16

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

dsc = img 'blank:32x32'..[[Строка с пустым изображением.]];
dsc = img 'box:32x32,red,128'..[[Строка красным полупрозрачным квадратом.]];

В современной версии INSTEAD вы можете использовать атрибут disp:

knife = obj {
	nam = 'Нож';
        disp = 'Нож'..img('img/knife.png'),
}

Начиная с версии 1.0.0 интерпретатор может обрабатывать составные картинки, например:

pic = 'gfx/mycat.png;gfx/milk.png@120,25;gfx/fish.png@32,32'

Интерпретатор проигрывает в цикле текущую музыку, которая задается с помощью функции: set_music(имя музыкального файла).

Например:

street = room {
	pic = 'gfx/street.png',
	enter = function()
		set_music('mus/rain.ogg');
	end,
	nam = 'на улице',
	dsc = 'На улице идет дождь.',
};

get_music() возвращает текущее имя трека.

Начиная с версии 0.7.7 в функцию set_music() можно передавать второй параметр – количество проигрываний. Получить текущий счетчик можно с помощью get_music_loop. 0 - означает вечный цикл. 1..n – количество проигрываний. -1 – проигрывание текущего трека закончено.

Начиная с версии 0.9.2 set_sound() позволяет проиграть звуковой файл. get_sound() возвращает имя звукового файла, который будет проигран.

Для того, чтобы отменить проигрывание, вы можете использовать stop_music() (с версии 1.0.0).

is_music() позволяет узнать, проигрывается ли музыка. (с версии 1.0.0)

18. Полезные советы

Разбиение на файлы

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

-- main.lua
dofile "episode1.lua"
dofile "npc.lau"
dofile "start.lua"

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

...
act = code [[ gamefile ("episode2.lua"); ]]
...

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

...
act = code [[ gamefile ("episode3.lua", true); ]]
...

Модули

Подробнее о модулях

Начиная с версии 1.2.0 появилась возможность использования модулей с помощью require. На данный момент существуют следующие модули:

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

--$Name: Моя игра!$
instead_version "1.2.0"
require "para"
require "dbg"
...

Следующие модули подключаются автоматически, если вы задали version >= 1.2.0: vars, object, goto.

Объект prefs (находится в модуле prefs) служит для сохранения настроек игры, например, его можно использовать для реализации системы достижений или счетчика количества прохождений…

  require "prefs"
...
    prefs.counter = 0
...
    exit = function(s)
        prefs.counter = prefs.counter + 1
        prefs:store()
    end
...
    enter = function(s)
        return 'Вы прошли игру '..prefs.counter..' раз(а)';
    end
...
    act = function(s)
        prefs:purge()
        return "Настройки обнулены"
    end

Модуль xact позволяет делать ссылки на объекты из других объектов, реакций и life методов в форме {объект|строка} следующим образом:

...
    act = [[ Под столом я заметил {knife|нож}.]]
...

При этом, объект может быть объектом или именем объекта.

Примечание: до версии 1.2.2 использовался следующий формат ссылок: {объект:строка}.

В этом модуле определены также такие объекты как xact и xdsc.

xact – объект - простейшая реакция. Например:

main = room {
    forcedsc = true;
    dsc = [[От автора. Эту игру я писал очень  {note1|долго}.]];
    obj = {
        xact('note1', [[Больше 10 лет.]]);
    }
}

Реакция может содержать код:

        xact('note1', code [[p "Больше 10 лет."]]);

xdsc позволяет вставить в список объектов множественное описание:

main = room {
    forcedsc = true;
    dsc = [[Я в комнате.]];
    xdsc = [[ Я вижу {apple|яблоко} и {knife|нож}. ]];
    other = [[ Еще здесь лежат {chain|цепь} и {tool|пила}.]];
    obj = {
        xdsc(), -- 'xdsc method by default'
        xdsc('other'),
        'apple', 'knife', 'chain', 'tool',
    }
}

Вы можете также использовать комнату xroom:

main = xroom {
    forcedsc = true;
    dsc = [[Я в комнате.]];
    xdsc = [[ Я вижу {apple|яблоко} и {knife|нож}. ]];
    obj = {
        'apple', 'knife', 'chain', 'tool',
    }
}

Модуль input позволяет реализовывать простые поля ввода, а click отслеживает щелчки по картинке сцены.

Модуль format выполняет форматирование вывода. По умолчанию все настройки выключены :

format.para = false -- отступы в начале абзаца;
format.dash = false -- замена двойного - на тире;
format.quotes = false -- замена " " на << >>;
format.filter = nil -- пользовательская функция замены;

Вы можете пользоваться модулями para, dash, quotes для включения отдельных настроек.

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

Вы можете делать простое форматирование текста с помощью функций:

Например:

main = room {
	nam = 'Intro',
	dsc = txtc('Добро пожаловать!'),
}

Вы также можете менять оформление текста с помощью комбинаций функций:

Например:

main = room {
	nam = 'Intro',
	dsc = 'Вы находитесь в комнате '..txtb('main')..'.',
}

Начиная с версии 1.1.0 вы также можете создавать неразрываемые строки с помощью: txtnb().

Меню

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

pocket = menu {
	State = false,
	nam = function(s)
		if s.State then
			return txtu('карман');
		end 
		return 'карман';
	end,
	gen = function(s)
		if s.State then
			s:enable_all();
		else
			s:disable_all();
		end 
	end,
	menu = function(s)
		if s.State then
			s.State = false;
		else
			s.State = true;
		end 
		s:gen();
	end,
};
 
knife = obj {
	nam = 'нож',
	inv = 'Это нож',
};
 
function init()
    inv():add(pocket);
    put(knife, pocket);
    pocket:gen();
end
 
main = room {
	nam = 'test',
};

Статус игрока

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

global {
    life = 10;
    power = 10;
}
 
status = stat {
	nam = function(s)
		p ('Жизнь: ', life, 'Сила: ', power)
	end
};
function init()
    inv():add('status');
end

goto из обработчика exit

Если вы выполните goto из обработчика exit, то получите переполнение стека, так как goto снова и снова будет вызывать метод exit. Вы можете избавиться от этого, если вставите проверку, разрушающую рекурсию. Например:

exit = function(s, t)
	if t == 'dialog' then return; end
	return goto('dialog');
end

Начиная с версии 0.9.1 движок сам разрывает рекурсию.

Вы можете также делать goto из обработчиков enter.

Динамически создаваемые ссылки.

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

objs(home):add(vway('Дорога', 'Я заметил {дорогу}, ведущую в лес...', 'forest'));

Для удаления ссылки можно использовать метод del.

objs(home):del('Дорога');

Для определения наличия ссылки в сцене – метод srch.

if not objs(home):srch('Дорога') then
	objs(home):add(vway('Дорога', 'Я заметил {дорогу}, ведущую в лес...', 'forest'));
end

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

if not seen('Дорога') then
	objs():add(vway('Дорога', 'Я заметил {дорогу}, ведущую в лес...', 'forest'));
end

Кроме того, вы можете просто включать и выключать ссылки с помощью enable(), disable(), например:

	seen('Дорога', home):disable();
        exsist('Дорога', home):enable();

Вы можете создавать выключенные vobj и vway следующим образом:

	obj = {vway('Дорога', 'Я заметил {дорогу}, ведущую в лес...', 'forest'):disable()},

И затем включать их по индексу в массиве obj или иным способом (seen/srch/exist):

	objs()[1]:enable();

Кодирование исходного кода игры (начиная с версии 0.9.3)

Если вы не хотите показывать исходный код своих игр, вы можете закодировать исходный код с помощью sdl-instead -encode <путь к файлу> [выходной путь] и использовать его с помощью lua функции doencfile. При этом главный файл main.lua необходимо оставлять текстовым. Таким образом схема выглядит следующим образом (game – закодированный game.lua):

-- $Name: Моя закрытая игра!$
doencfile("game");

Не используйте компиляцию игр с помощью luac, так как luac создает платформозависимый код! Однако, компиляция игр может быть использована для поиска ошибок в коде.

Запаковка ресурсов (начиная с версии 1.4.0)

Вы можете упаковать ресурсы игры (графику, музыку, темы) в файл .idf, для этого поместите все ресурсы в каталог data и запустите INSTEAD:

instead -idf <путь к data>

При этом в текущем каталоге должен будет создастся файл data.idf. Поместите его в каталог с игрой. Теперь ресурсы игры в виде отдельных файлов можно удалить.

Вы можете запаковать в формат .idf всю игру:

instead -idf <путь к игре>

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

instead game.idf

Переключение между игроками

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

Использование первого параметра обработчика

Пример кода.

stone = obj {
	nam = 'камень',
	dsc = 'На краю лежит {камень}.',
	act = function()
		objs():del('stone');
		return 'Я толкнул камень, он сорвался и улетел вниз...';
	end

Обработчик act мог бы выглядеть проще:

	act = function(s)
		objs():del(s);
		return 'Я толкнул камень, он сорвался и улетел вниз...';
	end

Использование set_music

Вы можете использовать set_music для проигрывания звуков, задавая второй параметр – счетчик циклов проигрывания звукового файла.

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

-- играет треки в случайном порядке, начиная со 2-го
tracks = {"mus/astro2.mod", "mus/aws_chas.xm", "mus/dmageofd.xm", "mus/doomsday.s3m"}
mplayer = obj {
	nam = 'плеер',
	life = function(s)
		local n = get_music();
		local v = get_music_loop();
		if not n or not v then
			set_music(tracks[2], 1);
		elseif v == -1 then
			local n = get_music();
			while get_music() == n do
				n = tracks[rnd(4)]
			end
			set_music(n, 1);
		end
	end,
};
lifeon('mplayer');

Вы можете использовать функции get_music_loop и get_music, для того, чтобы запоминать прошлую мелодию, и потом восстанавливать ее, например:

function save_music(s)
	s._oldMusic = get_music();
	s._oldMusicLoop = get_music_loop();
end
 
function restore_music(s)
	set_music(s._oldMusic, s._oldMusicLoop);
end
 
-- ....
enter = function(s)
	save_music(s);
end,
exit = function(s)
	restore_music(s);
end,
-- ....

Начиная с версии 0.8.5 функции save_music и restore_music уже присутствуют в библиотеке.

Живые объекты

Если вашему герою нужен друг, одним из способов может стать метод life этого персонажа, который всегда переносит объект в локацию игрока:

horse = obj {
	nam = 'лошадь',
	dsc = 'Рядом со мной стоит {лошадь}.',
	life = function(s)
		if not seen('horse') then
			move('horse', here(), s.__where);
			s.__where = pl.where;
		end
	end,
};
function init()
    lifeon('horse');
end

Таймер

Начиная с версии 1.1.0 в instead появлилась возможность использовать таймер. (Только в графической версии интерпретатора.)

Таймер программируется с помощью объекта timer.

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

timer.callback = function(s)
    main._time = main._time + 1;  
    return "look";
end
timer:set(100);
main = room {
    _time = 1,
    forcedsc = true,
    nam = 'Таймер',
    dsc = function(s)
	return 'Демонстрация: '..tostring(s._time);
    end
};

Клавиатура

Начиная с версии 1.1.0 в instead появилась возможность анализировать ввод с клавиатуры (только в графической версии интерпретатора). Для этого используется объект input.

input.key(s, pressed, key) – обработчик клавиатуры; pressed – нажатие или отжатие. key – символьное имя клавиши;

Обработчик может вернуть команду интерфейса stead, в этом случае клавиша не будет обработана интерпретатором. Например:

input.key = function(s, pr, key)
	if not pr or key == "escape"then 
		return 
	elseif key == 'space' then 
		key = ' ' 
	elseif key == 'return' then
		key = '^';
	end 
	if key:len() > 1 then return end 
	main._txt = main._txt:gsub('_$','');
	main._txt = main._txt..key..'_';
	return "look";
end
 
main = room {
	_txt = '_',
	forcedsc = true,
	nam = 'Клавиатура',
	dsc = function(s)
		return 'Демонстрация: '..tostring(s._txt);
	end 
};

Мышь

Начиная с версии 1.1.5 в instead появилась возможность анализировать события мыши. (Только в графической версии интерпретатора.) Для этого используется объект input.

input.click(s, pressed, mb, x, y, px, py) – обработчик клика мыши; pressed – нажатие или отжатие. mb – номер кнопки (1 - левая), x и y – координаты клика относительно левого верхнего угла. px и py присутствуют, если клик произошел в области картинки сцены и содержит координаты клика относительно левого верхнего угла картинки.

Обработчик может вернуть команду интерфейса stead, в этом случае клик не будет обработан интерпретатором.

Например:

input.click = function(s, press, mb, x, y, px, py)
	if press and px then
		click.x = px;
		click.y = py;
		click:enable();
		return "look"
	end
end
 
click = obj {
	nam = 'клик',
	x = 0,
	y = 0,
	dsc = function(s)
		return "Вы кликнули по картинке в позиции: "..s.x..','..s.y..'.';
	end
}:disable();
 
main = room {
	nam = 'test',
	pic ='picture.png',
	dsc = 'Демонстрация.',
	obj = { 'click' },
};

Пример прослойки, которая реализует вызов метода click в текущей комнате при клике на картинку:

input.click = function(s, press, mb, x, y, px, py)
	if press and px then
		return "click "..px..','..py;
	end
end
 
game.action = function(s, cmd, x, y)
	if cmd == 'click' then
		return call(here(), 'click', x, y);
	end
end
----------------------------------------------------------------------
main = room {
	nam = 'test',
	pic ='picture.png',
	dsc = 'Демонстрация.',
	click = function(s, x, y)
		return "Вы кликнули по картинке в позиции: "..x..','..y..'.';
	end
};

Внимание!!! Начиная с 1.2.0 рекомендуется использовать модуль click.

Динамическое создание объектов

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

new ("obj { nam = 'test', act = 'test' }")
put(new [[obj {nam = 'test' } ]]);
put(new('myconstructor()');
n = new('myconstructor()');
delete(n)

new воспринимает строку-аргумент как конструктор объекта. Результатом выполнения конструктора должен быть объект. Таким образом в аргументе обычно задан вызов функции-конструктора. Например:

function myconstructor()
	local v = {}
	v.nam = 'тестовый объект',
	v.act = 'Тестовая реакция',
	return obj(v);
end

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

o_name = deref(new('myconstructor()'));
delete(o_name);

Сложный вывод из обработчиков

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

dsc = function(s)
	p "На полу стоит {бочка}."
	if s._opened then
		p "Крышка от бочки лежит рядом."
	end
end

Функция pn() выполняет вывод текста в буфер, дополняя его переводом строки. Функция p() дополняет вывод пробелом.

Начиная с версии 1.1.6 существует функция pr(), которая не выполняет дополнение вывода.

Для очистки буфера, используйте pclr(). Если вам нужно вернуть статус действия, используйте pget(), или просто используйте return.

use = function(s, w)
	if w == apple then
		p 'Гм... Я почистил яблоко.';
		apple._peeled = true
		return
	end
	p 'Это нельзя использовать так!'
	return false; -- или return pget(), false
end

Отладка

Для того, чтобы во время ошибки увидеть стек вызовов функций lua, вы можете запустить sdl-instead с параметром -debug. При этом в windows версии интерпретатора будет создана консоль отладки.

Вы можете отлаживать свою игру вообще без instead. Например, вы можете создать следующий файл game.lua:

dofile("/usr/share/games/stead/stead.lua"); -- путь к stead.lua
dofile("main.lua"); -- ваша игра
game:ini();
iface:shell();

И запустите игру в lua: lua game.lua. При этом игра будет работать в примитивном shell окружении. Полезные команды: ls, go, act, use

Для включения простого отладчика, после version вначале файла напишите:

require "dbg"

Отладчик вызывается по F7.

19. Темы для sdl-instead

Графический интерпретатор поддерживает механизм тем. Тема представляет из себя каталог, с файлом theme.ini внутри.

Тема, которая является минимально необходимой – это тема default. Эта тема всегда загружается первой. Все остальные темы наследуются от нее и могут частично или полностью заменять ее параметры. Выбор темы осуществляется пользователем через меню настроек, однако конкретная игра может содержать собственную тему и таким образом влиять на свой внешний вид. В этом случае в каталоге с игрой должен находиться свой файл theme.ini. Тем не-менее пользователь свободен отключить данный механизм, при этом интерпретатор будет предупреждать о нарушении творческого замысла автора игры.

Синтаксис theme.ini очень прост.

<параметр> = <значение> 

или

; комментарий

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

Цвет задается в форме #rgb, где r g и b компоненты цвета в шестнадцатеричном виде. Кроме того некоторые основные цвета распознаются по своим именам. Например: yellowgreen, или violet.

Параметры могут принимать значения:

Кроме того, заголовок темы может включать в себя комментарии с тегами. На данный момент существует только один тег: $Name:, содержащий UTF-8 строку с именем темы. Например:

; $Name:Новая тема$
; модификация темы book
include = book
scr.gfx.h = 500

Интерпретатор выполняет поиск тем в каталоге themes. Unix версия кроме этого каталога, просматривает также каталог ~/.instead/themes/ Windows версия (>=0.8.7): Documents and Settings/USER/Local Settings/Application Data/instead/themes