Skip to content

Latest commit

 

History

History
5847 lines (4682 loc) · 264 KB

stead3-ru.md

File metadata and controls

5847 lines (4682 loc) · 264 KB

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

Код игр под INSTEAD пишется на языке Lua (5.1), поэтому, знание этого языка полезно, хотя и не необходимо. Ядро движка также написано на lua, поэтому знание Lua может быть полезным для углублённого понимания принципов его работы, конечно, при условии, если вам интересно этим заниматься.

За время своего развития, INSTEAD получил множество новых функций. Теперь с его помощью можно делать игры разных жанров (от аркад, до игр с текстовым вводом). А также, в INSTEAD можно запускать игры, написанные на некоторых других движках, но основой INSTEAD остаётся первоначальное ядро, которое ориентировано на создание тексто-графических приключенческих игр. В данной документации описана именно эта базовая часть, изучение которой необходимо даже в том случае, если вы хотите написать что-то другое... Начните своё изучение INSTEAD с написания простой игры!

В феврале 2017 года, после 8 лет разработки, INSTEAD (версия 3.0) вышел с поддержкой нового ядра STEAD3. Старое ядро получило название STEAD2. INSTEAD поддерживает работу игр, написанных как на STEAD2, так и на STEAD3. Это руководство описывает STEAD3.

Если у вас возникают вопросы, вы можете посетить сайт INSTEAD:

https://instead-hub.github.io

История создания

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

Вы видите перед собой стол. На столе лежит яблоко. Что делать?

1) Взять яблоко
2) Отойти от стола

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

Вы на кухне. Тут есть стол.
> осмотреть стол.
На столе есть яблоко.

У обоих подходов есть свои преимущества и недостатки.

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

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

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

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

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

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

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

Если вас интересует история создания движка, то вы можете прочитать статью о том, как всё начиналось: https://instead-hub.github.io/article/2010-05-09-history/

Как выглядит классическая INSTEAD игра

Итак, как выглядит классическая INSTEAD игра?

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

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

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

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

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

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

  • осмотр сцены;
  • действие на объект сцены;
  • действие на объект инвентаря;
  • действие объектом инвентаря на объект инвентаря;
  • действие объектом инвентаря на объект сцены;
  • переход в другую сцену.

Как создавать игру

Игра представляет из себя каталог, в котором должен находиться скрипт (текстовый файл) main3.lua. (Обратите внимание, наличие файла main3.lua означает, что вы пишите игру на STEAD3!) Другие ресурсы игры (скрипты на lua, графика и музыка) должны находиться в рамках этого каталога. Все ссылки на ресурсы делаются относительно текущего каталога -- каталога игры.

В начале файла main3.lua может быть определён заголовок, состоящий из тегов (строк специального вида). Теги должны начинаться с символов: два минуса.

--

Два минуса -- это комментарий с точки зрения Lua. На данный момент существуют следующие теги.

Тег $Name: содержит название игры в кодировке UTF-8. Пример использования тега:

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

Затем следует (желательно) задать версию игры:

-- $Version: 0.5$

И указать авторство:

-- $Author: Анонимный любитель текстовых приключений$

Дополнительную информацию об игре, можно задать тегом Info:

-- $Info: Это ремейк старой игры\nС ZX specturm$

Обратите внимание на \n в Info, это станет переводом строки, когда вы выберете пункт "Информация" в INSTEAD.

Если вы разрабатываете игру в Windows, то убедитесь, что ваш редактор поддерживает кодировку UTF-8 без BOM. Именно эту кодировку следует использовать при написании игры!

Далее, обычно следует указать модули, которые требуются игре. О модулях будет рассказано отдельно.

require "fmt" -- некоторые функции форматирования
fmt.para = true -- включить режим параграфов (отступы)

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

game.act = 'Не работает.';
game.use = 'Это не поможет.';
game.inv = 'Зачем мне это?';

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

function init() -- добавим в инвентарь нож и бумагу
    take 'нож'
    take 'бумага'
end

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

function start(load) -- восстановить состояние?
	if load then
		dprint "Это загрузка состояния."
	else
		dprint "Это старт игры."
	end
	-- нам сейчас не нужно ничего делать
end

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

В некоторых сборках INSTEAD (в Windows, в Linux если проект собран с gtk и др.) можно открывать игру по любому пути из меню "Выбор игры". Либо, нажать f4. Если в каталоге с играми присутствует только одна игра, INSTEAD запустит её автоматически, это удобно, если вы хотите распространять свою игру вместе с движком.

Таким образом, вы кладёте игру в свой каталог и запускаете INSTEAD.

Важно!

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

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

-- $Name: Моя первая игра$
-- $Version: 0.1$
-- $Author: Анонимный автор$

require "fmt"
fmt.para = true

game.act = 'Гм...';
game.use = 'Не сработает.';
game.inv = 'Зачем это мне?';

function init()
-- инициализация, если она нужна
end

Основы отладки

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

Кроме того, в режиме -debug автоматически подключается отладчик. Вы можете активировать его с помощью клавиш ctrl-d или f7. Вы можете подключить отладчик и явно указав:

require "dbg"

В коде вашей игры.

При отладке игры обычно нужно часто сохранять игру и загружать состояние игры. Вы можете использовать стандартный механизм сохранений через меню (или по клавишам f2/f3), или воспользоваться быстрым сохранением/загрузкой (клавиши f8/f9).

В режиме '-debug' вы можете перезапускать игру клавишами 'alt-r'. В комбинации с f8/f9 это даёт возможность быстро посмотреть изменения в игре после её правки.

Внимание! Если вы просто перезапустите INSTEAD, то скорее всего увидите старое состояние игры, так как по умолчанию работает режим автозагрузки автосохранения! Либо отключите эту настройку в меню (автосохранение), либо явно перезапускайте игру после правок. Перезапуск возможен через меню (начать заново) или 'alt-r' в режиме '-debug' как это описано выше.

В режиме '-debug' Windows-версия INSTEAD создаёт консольное окно (в Unix версии, если вы запускаете INSTEAD из консоли, вывод будет направлен в неё) в которое будет осуществляться вывод ошибок. Кроме того, используя функцию 'print()', вы сможете порождать свои сообщения с отладочным выводом. Например:

act = function(s)
	print ("Act is here! ");
    ...
end;

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

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

act = function(s)
	dprint ("Act is here! ");
    ...
end;

Во время отладки бывает удобно изучать файлы сохранений, которые содержат состояние переменных игры. Чтобы не искать каждый раз файлы сохранений, создайте каталог saves в директории с вашей игрой (в том каталоге, где содержится main3.lua) и игра будет сохраняться в saves. Этот механизм также будет удобен для переноса игры на другие компьютеры.

Возможно (особенно, если вы пользуетесь Unix системами) вам понравится идея проверки синтаксиса ваших скриптов через запуск компилятора ''luac''. В Windows это тоже возможно, нужно только установить выполняемые файлы lua для Windows (http://luabinaries.sourceforge.net)/ и воспользоваться luac52.exe.

Вы можете проверить синтаксис и с помощью INSTEAD, для этого воспользуйтесь параметром -luac:

sdl-instead -debug -luac <пусть к скрипту.lua>

Сцена

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

В любой игре должна быть сцена с именем ''main''. Именно с неё начнётся и ваша игра!

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

Запись означает создание объекта (так как почти все сущности в INSTEAD это объекты) main типа room (комната). Атрибут объекта nam хранит имя комнаты 'main', по которому можно обращаться к комнате из кода. Каждый объект имеет своё уникальное имя. Если вы попробуете создать два объекта с одинаковыми именами, вы получите сообщение об ошибке.

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

dprint("Объект: ", _'main')

У каждого объекта игры есть атрибуты и обработчики событий. В данном примере есть два атрибута: nam и dsc. Атрибуты разделяются разделителем (в данном примере -- символом точка с запятой ';').

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

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

room {
	disp = "Главная комната";
	dsc = [[Вы в большой комнате.]];
}

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

myroom = room {
	disp = "Чулан";
	dsc = [[Вы в чулане.]];
}

Переменная myroom в таком случае становится синонимом объекта (ссылкой на сам объект).

dprint("Объект: ", myroom)

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

main_room = room {
	nam = 'main';
	disp = "Главная комната";
	dsc = [[Вы в большой комнате.]];
}

Важно понять, что движок в любом случае работает с именами объектов, а переменные-ссылки -- это просто способ упростить доступ к часто используемым объектам. Поэтому, для нашей первой игры мы обязаны указать атрибут nam = 'main', чтобы создать комнату main с которой и начнётся наше приключение!

В нашем примере, при показе сцены, в качестве заголовка сцены будет использован атрибут 'disp'. На самом деле, если бы мы его не задали, то в заголовке мы бы увидели 'nam'. Но nam не всегда удобно делать заголовком сцены, особенно если это строка вроде 'main', или если это числовой идентификатор, который движок присвоил объекту автоматически.

Есть ещё более понятный атрибут 'title'. Если он задан, то при отображении комнаты в качестве заголовка будет указан именно он. title используется тогда, когда игрок находится внутри комнаты. Во всех остальных случаях (при показе переходов в эту комнату) используется 'disp' или 'nam'.

mroom = room {
	nam = 'main';
	title = 'Начало приключения';
	disp = "Главная комната";
	dsc = [[Вы в большой комнате.]];
}

Атрибут 'dsc' -- это описание сцены, которое выводится один раз при входе в сцену или при явном осмотре сцены. В нем нет описаний объектов, присутствующих в сцене.

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

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

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

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

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

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

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

dsc = [[ Первый абзац. ^^
Второй Абзац.^^

Третий абзац.^
На новой строке.]];

Я рекомендую всегда использовать [[ и ]] для 'dsc'.

Напомню ещё раз, что имя 'nam' объекта и его отображение (в данном случае то, как сцена будет выглядеть для игрока в виде надписи сверху) можно (и, часто, нужно!) разделять. Для этого существуют атрибуты 'disp' и 'title'. 'title' бывает только у комнат и работает как описатель, когда игрок находится внутри данной комнаты. В остальных случаях используется 'disp' (если он есть).

Если 'disp' и 'title' не заданы, то считается, что отображение равняется имени.

'disp' и 'title' могут принимать значение false, в таком случае, отображения не будет.

Объекты

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

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

Имя объекта ''nam'' используется при попадании его в инвентарь. Хотя, в нашем случае, стол вряд ли туда попадёт. Если у объекта определён 'disp', то при попадании в инвентарь для его отображения будет использоваться именно этот атрибут. Например:

obj {
    nam = 'стол';
    disp = 'угол стола';
    dsc = 'В комнате стоит {стол}.';
    tak = 'Я взялся за угол стола';
    inv = 'Я держусь за угол стола.';
};

Всё-таки стол попал к нам в инвентарь.

Вы можете скрывать отображение предмета в инвентаре, если 'disp' атрибут будет равен 'false'.

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

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

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

Для того, чтобы поместить в сцену объекты, существует несколько путей.

Во-первых, при создании комнаты можно определить список 'obj', состоящий из имён объектов:

obj { -- объект с именем, но без переменной
	nam = 'ящик';
	dsc = [[На полу я вижу {ящик}.]];
	act = [[Тяжёлый!]];
}

room {
	nam = 'main';
	disp = 'Большая комната';
	dsc = [[Вы в большой комнате.]];
	obj = { 'ящик' };
};

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

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

apple = obj { -- объект с переменной, но без имени
	dsc = [[Тут есть {яблоко}.]];
	act = [[Красное!!]];
}

room {
    nam = 'main';
	disp = 'Большая комната';
	dsc = [[Вы в большой комнате.]];
	obj = { apple };
};

Альтернативной формой записи является конструкция with:

room {
	nam = 'main';
	disp = 'Большая комната';
	dsc = [[Вы в большой комнате.]];
}:with {
    'ящик',
}

Конструкция with позволяет избавиться от лишнего уровня вложенности в коде игры.

Во-вторых, вы можете объявлять объекты прямо внутри obj или with, описывая их определение:

room {
	nam = 'main';
	disp = 'Большая комната';
	dsc = [[Вы в большой комнате.]];
}:with {
	obj {
		nam = 'ящик';
		dsc = [[На полу я вижу {ящик}.]];
		act = [[Тяжёлый!]];
	}
};

Это удобно делать для объектов - декораций. Но в таком случае, вы не сможете создавать объекты с переменной-ссылкой. К счастью, для декораций это и не нужно.

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

obj = { 'ящик', apple };

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

obj = {
    'table',
    'apple',
    'knife',
};

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

Декорации

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

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

Таких объектов может быть очень много, и более того, обычно это однотипные объекты вроде деревьев и тому подобных объектов.

Для создания декораций можно использовать различные подходы.

Один и тот же объект в нескольких комнатах

Вы можете создать один объект, например, 'дерево' и помещать их в разные комнаты.

obj {
	nam = 'дерево';
	dsc = [[Тут стоит {дерево}.]];
	act = [[Все деревья выглядят похожими.]];
}

room {
	nam = 'Лес';
	obj = { 'дерево' };
}

room {
	nam = 'Улица';
	obj = { 'дерево' };
}

Использование тегов вместо имён

Если вам не нравится придумывать уникальные имена для однотипных декоративных объектов, вы можете использовать для таких объектов теги. Теги задаются атрибутом tag и всегда начинаются с символа '#':

obj {
	tag = '#цветы';
	dsc = [[Тут есть {цветы}.]]
}

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

dprint(_'#цветы') -- ищем в текущей комнате первый объект с тегом '#цветы'

Теги, это в каком-то смысле, синоним локальных имён, поэтому существует альтернативная запись создания предмета с тегом:

obj {
	nam = '#цветы';
	dsc = [[Тут есть {цветы}.]]
}

Если имя у объекта начинается с символа '#', то такой объект получает тег и автоматически сгенерированное числовое имя.

Использование атрибута сцены decor

Так как декорации не меняют своё местоположение, есть смысл сделать их частью описания сцены, а не динамической области. Это делается с помощью атрибута сцены 'decor'. decor показывается всегда и его основная функция -- описание декораций сцены.

room {
	nam = 'Дом';
	dsc = [[Я у себя дома.]];
	decor = [[Тут я вижу много интересных вещей. Например, на {#стена|стене}
	висит {#картина|картина}.]];
}: with {
	obj {
		nam = '#стена';
		act = [[Стена как стена!]];
	};
	obj {
		nam = '#картина';
		act = [[Ван-Гог?]];
	}
}

Здесь мы видим сразу несколько приёмов:

  1. В decor в виде связанного текста описаны декорации;
  2. В качестве ссылок используются конструкции с явным заданием объектов, к которым они относятся {имя объекта|текст};
  3. В качестве имён объектов используются теги, чтобы не думать над их уникальностью;
  4. У объектов-декораций в сцене отсутствуют атрибуты dsc, так как их роль играет decor.

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

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

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

obj {
	nam = 'яблоко';
	dsc = [[На столе лежит {яблоко}.]];
	act = 'Взять что ли?';
};

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

room {
    nam = 'Дом';
	obj = { 'стол' };
}

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

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

Конечно, данный пример мог бы быть написан и по-другому, например, так:

room {
	nam = 'Дом';
}:with {
	obj {
		nam = 'стол';
		dsc = [[В комнате стоит {стол}.]];
		act = 'Гм... Просто стол...';
	}: with {
		obj {
			nam = 'яблоко';
			dsc = [[На столе лежит {яблоко}.]];
			act = 'Взять что ли?';
		};
	}
}

Выбирайте тот способ, который для вас понятней.

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

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

disp = function()
	p 'яблоко';
end

Пример не очень удачен, так как проще было бы написать disp = 'яблоко', но показывает синтаксис записи функции.

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

 return "яблоко";

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

Более привычным способом вывода являются функции:

  • p ("текст") -- вывод текста и пробела;
  • pn ("текст") -- вывод текста с переводом строки;
  • pr ("текст") -- вывод текста "как есть".

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

pn "Нет скобкам!"

Все эти функции дописывают текст в буфер и при возврате из функции возвращают его движку. Таким образом, вы можете постепенно формировать вывод за счёт последовательного выполнения p/pn/pr. Имейте в виду, что автору крайне редко необходимо явно форматировать текст, особенно, если это описание объектов, движок сам расставляет необходимые переводы строк и пробелы для разделения информации разного рода и делает это унифицированным способом.

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

pn ("Строка 1".." Строка 2");
pn ("Строка 1", "Строка 2");

Основное отличие атрибутов от обработчиков событий состоит в том, что обработчики событий могут менять состояние игрового мира, а атрибуты нет. Поэтому, если вы оформляете атрибут (например, 'dsc') в виде функции, помните, что задача атрибута -- это возврат значения, а не изменение состояния игры! Дело в том, что движок обращается к атрибутам в те моменты времени, которые обычно чётко не определены, и не связаны явно с какими-то игровыми процессами!

Важно!

Ещё одной особенностью обработчиков является тот факт, что вы не должны ждать каких-то событий внутри обработчика. То есть, не должно быть каких-то циклов ожидания, или организации задержек (пауз). Дело в том, что задача обработчика -- изменить игровое состояние и отдать управление INSTEAD, который визуализирует эти изменения и снова перейдёт в ожидание действий пользователя. Если вам требуется организовать задержки вывода, вам придётся воспользоваться модулем "timer".

Функции практически всегда содержат условия и работу с переменными. Например:

obj {
	nam = 'яблоко';
	seen = false;
	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) -- сам объект. То-есть, в данном примере, 's' это синоним _'яблоко'. Когда вы работаете с самим объектом в функции, удобнее использовать параметр, а не явное обращение к объекту по имени, так как при переименовании объекта вам не придётся переписывать вашу игру. Да и запись будет короче.

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

Как видим, синтаксис оператора 'if' довольно очевиден. Для наглядности, несколько примеров.

if <выражение> then <действия> end

if have 'яблоко' then
	p 'У меня есть яблоко!'
end

if <выражение> then <действия> else <действия иначе> end

if have 'яблоко' then
	p 'У меня есть яблоко!'
else
	p 'У меня нет яблока!'
end

if <выражение> then <действия> elseif <выражение 2> then <действия 2> else <иначе> end -- и т.д.

if have 'яблоко' then
	p 'У меня есть яблоко!'
elseif have 'вилка' then
	p 'У меня нет яблока, но есть вилка!'
else
	p 'У меня нет ни яблока, ни вилки!'
end

Выражение в операторе if может содержать логическое "и" (and), "или" (or), "отрицание" (not) и скобки ( ) для задания приоритетов. Запись вида if <переменная> then означает, что переменная не равна false. Равенство описывается как '==', неравенство '~='.

if not have 'яблоко' and not have 'вилка' then
    p 'У меня нет ни яблока, ни вилки!'
end

...
if w ~= apple then
   p 'Это не яблоко.';
end
...

if time() == 10 then
   p '10 й ход настал!'
end

Важно!

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

Переменные объекта

Запись 's.seen' означает, что переменная 'seen' размещена в объекте 's' (то есть 'яблоко'). Помните, мы назвали первый параметр функции 's' (от self), а первый параметр -- это сам текущий объект.

Переменные объекта должны быть определены заранее, если вы собираетесь их модифицировать. Примерно так, как мы поступили с seen. Но переменных может быть много.

obj {
	nam = 'яблоко';
	seen = false;
	eaten = false;
	color = 'красный';
	weight = 10;
	...
};

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

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

obj {
	nam = 'яблоко';
	{
	   t = 1; -- эта переменная не попадёт в сохранения
	   x = false; -- и эта тоже
	}
};

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

obj {
	nam = 'яблоко';
	{
	   text = { "раз", "два", "три" }; -- никогда не попадёт в файл сохранения
	}
	...
};

Вы можете обращаться к переменным объекта через s (если это сам объект) или по переменной - ссылке, например:

apple = obj {
    color = 'красный';
}
...
-- где-то в другом месте
    apple.color = 'зелёный'

Или по имени:

obj {
    nam = 'яблоко';
    color = 'красный';
}
...
-- где-то в другом месте
    _'яблоко'.color = 'зелёный'

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

apple 'xxx' (10) -- создали переменную xxx у объекта apple по ссылке
(_'яблоко') 'xxx' (10) -- то же самое, но по имени объекта

Локальные переменные

Кроме переменных объекта, вы можете использовать локальные и глобальные переменные.

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

act = function(s)
    local w = _'лампочка'
    w.light = true
    p [[Я нажал на кнопку, и лампочка загорелась.]]
end

В данном примере, переменная w существует только в теле функции-обработчика act. Мы создали временную ссылку-переменную w, которая ссылается на объект 'лампочка', чтобы изменить свойство-переменную light у этого объекта.

Конечно, мы могли написать и:

_'лампочка'.light = true

Но представьте себе, если нам нужно произвести несколько действий с объектом, в таких случаях проще воспользоваться временной переменной.

Локальные переменные никогда не попадают в файл-сохранение и играют роль временных вспомогательных переменных.

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

Ещё один пример использования локальных переменных:

obj {
	nam = 'котёнок';
	state = 1;
	act = function(s)
		s.state = s.state + 1
		if s.state > 3 then
			s.state = 1
		end
		p [[Муррр!]]
	end;
	dsc = function(s)
		local dsc = {
			"{Котёнок} мурлычет.",
			"{Котёнок} играет.",
			"{Котёнок} облизывается.",
		};
		p(dsc[s.state])
	end;
end

Как видим, в функции dsc мы определили массив dsc. 'local' указывает на то, что он действует в пределах функции dsc. Конечно, данный пример можно было написать и так:

dsc = function(s)
    if s.state == 1 then
        p "{Котёнок} мурлычет."
    elseif s.state == 2 then
        p "{Котёнок} играет."
    else
        p "{Котёнок} облизывается.",
    end
end

Глобальные переменные

Вы также можете создать глобальную переменную:

global { -- определение глобальных переменных
    global_var = 1; -- число
    some_number = 1.2; -- число
    some_string = 'строка';
    know_truth = false; -- булево значение
    array = {1, 2, 3, 4}; -- массив
}

Ещё одна форма записи, удобная для одиночных определений:

global 'global_var' (1)

Глобальные переменные всегда попадают в файл-сохранение.

Кроме глобальных переменных, вы можете задавать константы. Синтаксис аналогичен глобальным переменным:

const {
	A = 1;
	B = 2;
}
const 'Aflag' (false)

Движок будет контролировать неизменность констант. Константы не попадают в файл-сохранение.

Иногда вам нужно работать с переменной, которая не определена как local (и видима во всех ваших lua файлах игры), но не должна попадать в файл сохранения. Для таких переменных вы можете использовать декларации:

declare {
	A = 1;
	B = 2;
}
declare 'Z' (false)

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

declare 'test' (function()
	p "Hello world!"
end)

global 'f' (test)

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

Вы можете декларировать ранее определённые функции, например:

declare 'dprint' (dprint)

Тем самым делая такие недекларированные функции -- декларированными.

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

Вспомогательные функции

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

function mprint(n, ...)
	local a = {...}; -- временный массив с аргументами к функции
	p(a[n]) -- выведем n-й элемент массива
end
...
dsc = function(s)
	mprint(s.state, {
		"{Котёнок} мурлычет.",
		"{Котёнок} играет.",
		"{Котёнок} облизывается." });
end;

Пока не обращайте внимания на данный пример, если он кажется вам сложным.

Возвращаемые значения обработчиков

Если необходимо показать, что действие не выполнено (обработчик не сделал ничего полезного), возвращайте значение false. Например:

act = function(s)
	if broken_leg then
		return false
	end
	p [[Я ударил ногой по мячу.]]
end

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

game.act = 'Гм... Не получается...';

Итак, если вы не задали обработчик act или вернули из него false -- считается, что реакции нет и движок выполнит аналогичный обработчик у объекта 'game'.

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

На самом деле, кроме 'game.act' и 'act' атрибутов объекта, существует обработчик 'onact' у объекта game, который может прервать выполнение обработчика 'act'.

Перед тем как вызвать обработчик 'act' у объекта, вызывается onact у game. Если обработчик вернёт false, выполнение 'act' обрывается. 'onact' удобно использовать, для контроля событий в комнате или игре, например:

-- вызываем onact комнат, если они есть
-- для действий на любой объект

game.onact = function(s, ...)
	local r, v = std.call(here(), 'onact', ...)
	if v == false then -- если false, обрубаем цепочку
		return r, v
	end
	return
end

room {
	nam = 'shop';
	disp = 'Магазин';
	onact = function(s, w)
		p [[В магазине нельзя воровать!]]
		p ([[Даже, если это всего лишь ]], w, '.')
		return false
	end;
	obj = { 'мороженное', 'хлеб' };
}

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

Всё, что описано выше на примере 'act' действует и для других обработчиков: tak, inv, use, а также при переходах, о чём будет рассказано далее.

Иногда возникает необходимость вызвать функцию - обработчик вручную. Для этого используется синтаксис вызова метода объекта. 'Объект:метод(параметры)'. Например:

apple:act() -- вызовем обработчик 'act' у объекта 'apple' (если он
определён как функция!).  _'яблоко':act() -- то же самое, но по
имени, а не по переменной-ссылке

Такой метод работает только в том случае, если вызываемый метод оформлен как функция. Вы можете воспользоваться 'std.call()' для вызова обработчика тем способом, каким это делает сам INSTEAD. (Будет описано в дальнейшем).

Инвентарь

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

Например:

obj {
	nam = 'яблоко';
	dsc = 'На столе лежит {яблоко}.';
	inv = function(s)
		p 'Я съел яблоко.'
		remove(s); -- удалить яблоко из инвентаря
	end;
	tak = 'Вы взяли яблоко.';
};

При этом, при действии игрока на объект "яблоко" (щелчок мыши на ссылку в сцене) -- яблоко будет убрано из сцены и добавлено в инвентарь. При действии игрока на инвентарь (двойной щелчок мыши на названии объекта) -- вызывается обработчик 'inv'.

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

Конечно, мы могли бы реализовать код взятия объекта в ''act'', например, так:

obj {
	nam = 'яблоко';
	dsc = 'На столе лежит {яблоко}.';
	inv = function(s)
		p 'Я съел яблоко.'
		remove(s); -- удалить яблоко из инвентаря
	end;
	act = function(s)
		take(s)
		p 'Вы взяли яблоко.';
	end
};

Если у объекта в инвентаре не объявлен обработчик 'inv', будет вызван 'game.inv'.

Если обработчик 'tak' вернёт false, то предмет не будет взят, например:

obj {
	nam = 'яблоко';
	dsc = 'На столе лежит {яблоко}.';
	tak = function(s)
		p "Оно же червивое!"
		return false
	end;
};

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

Традиционные переходы в INSTEAD выглядят как ссылки над описанием сцены. Для определения таких переходов между сценами используется атрибут сцены -- список 'way'. В списке определяются комнаты, в виде имён комнат или переменных-ссылок, аналогично списку 'obj'. Например:

room {
	nam = 'room2';
	disp = 'Зал';
	dsc = 'Вы в огромном зале.';
	way = { 'main' };
};

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

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

room {
	nam = 'room2';
	disp = 'В зал';
	title = 'В зале';
	dsc = 'Вы в огромном зале.';
	way = { 'main' };
};

room {
    nam = 'main';
	title = 'В главной комнате';
	disp = 'В главную комнату';
	dsc = 'Вы в большой комнате.';
	way = { 'room2' };
};

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

room {
	onenter = 'Вы заходите в зал.';
	nam = 'Зал';
	dsc = 'Вы в огромном зале.';
	way = { 'main' };
	onexit = 'Вы выходите из зала.';
};

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

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

Запись вида:

if f^'main' then

Это сопоставление объекта с именем. Это альтернатива записям:

if f == _'main' then

Или:

if f.nam == 'main' then

Или:

if std.nameof(f) == 'main' then

Как видим на примере onexit, эти обработчики, кроме строки, могут возвращать булевое значение статуса. Аналогично обработчику onact, мы может отменить переход, вернув false из onexit/onenter.

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

	return "Я не хочу назад", false

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

Важно!

Следует отметить, что при вызове обработчика 'onenter' указатель на текущую сцену (here()) ещё не изменён!!! В INSTEAD есть обработчики 'exit' (уход из комнаты) и 'enter' (заход в комнату), которые вызываются уже после того, как переход произошёл. Эти обработчики рекомендованы к использованию всегда, когда нет необходимости запрещать переход.

Иногда есть необходимость, чтобы название перехода отличалось от названия комнаты, в которую ведёт этот переход. Существует несколько способов сделать это. Например, с помощью 'path'.

room {
	nam = 'room2';
	title = 'Зал';
	dsc = 'Вы в огромном зале.';
	way = { path { 'В главную комнату', 'main'} };
};

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

На самом деле, 'path' создаёт комнату с атрибутом 'disp', который равен первому параметру, и специальной функцией 'onenter', которая перенаправляет игрока в комнату, заданную вторым параметром 'path'.

Если вы укажете три параметра:

way = { path {'#взал', 'В зал', 'room2'} };

То первый параметр станет именем (или тегом, как в приведённом примере) такой комнаты.

Альтернативная форма записи с явным заданием атрибута nam:

way = { path { nam = '#взал', 'В зал', 'room2'} };

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

way = { path {'#вдверь', 'В дверь', after = 'В гостиную', 'room2'} };

Все параметры, кроме имени перехода, могут быть функциями.

Таким образом, 'path' позволяет именовать переходы удобным способом.

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

Нет особого смысла прятать переход "дверь". Просто в функции 'onenter' сцены внутри дома мы проверяем, а есть ли у героя ключ? И если ключа нет, говорим о том, что дверь закрыта и запрещаем переход. Это повышает интерактивность и упрощает код. Если же вы хотите сделать дверь объектом сцены, поместите её в комнату, но в 'act' обработчике сделайте осмотр двери, или дайте возможность игроку открыть её ключом (как это сделать - мы рассмотрим позже), но сам переход дайте сделать игроку привычным способом через строку переходов.

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

obj {
	nam = 'часы';
	dsc = [[Тут есть старинные {часы}.]];
	act = function(s)
		enable '#часы'
		p [[Вы видите, что в часах есть потайной ход!]];
	end;
}

room {
	nam = 'Зал';
	dsc = 'Вы в огромном зале.';
	obj = { 'часы' };
	way = { path { '#часы', 'В часы', 'inclock' }:disable() };
};

В данном примере, мы создали отключённый переход, за счёт вызова метода 'disable' у комнаты, созданной с помощью 'path'. Метод 'disable' есть у всех объектов (не только комнат), он переводит объект в отключённое состояние, которое означает, что объект перестаёт быть доступным игроку. Замечательным свойством отключённого объекта является то, что его можно включить с помощью 'enable()';

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

Альтернативный вариант заключается не в выключении, а 'закрытии' объекта:

obj {
	nam = 'часы';
	dsc = [[Тут есть старинные {часы}.]];
	act = function(s)
		open '#часы'
		p [[Вы видите, что в часах есть потайной ход!]];
	end;
}

room {
	nam = 'Зал';
	dsc = 'Вы в огромном зале.';
	obj = { 'часы' };
	way = { path { '#часы', 'В часы', 'inclock' }:close() };
};

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

Закрытие объекта делает недоступным содержимое данного объекта, но не сам объект.

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

Ещё один вариант:

room {
	nam = 'inclock';
	dsc = [[Я в часах.]];
}:close()

obj {
	nam = 'часы';
	dsc = [[Тут есть старинные {часы}.]];
	act = function(s)
		open 'inclock'
		p [[Вы видите, что в часах есть потайной ход!]];
	end;
}

room {
	nam = 'Зал';
	dsc = 'Вы в огромном зале.';
	obj = { 'часы' };
	way = { path { 'В часы', 'inclock' } };
};

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

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

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

Например:

obj {
	nam = 'нож';
	dsc = 'На столе лежит {нож}';
	inv = 'Острый!';
	tak = 'Я взял нож!';
	use = 'Вы пытаетесь использовать нож.';
};

obj {
	nam = 'стол';
	dsc = 'В комнате стоит {стол}.';
	act = 'Гм... Просто стол...';
	obj = { 'нож' };
	used = function(s)
		p 'Вы пытаетесь сделать что-то со столом...';
		return false
	end;
};

В данном примере, обработчик used возвращает false. Зачем? Если вы помните, возврат false означает, что обработчик сообщает движку о том, что событие он не обработал. Если бы мы не вернули бы false, очередь до обработчика 'use' объекта 'нож' просто бы не дошла. На самом деле, в реальности обычно вы будете пользоваться или use или used, вряд ли имеет смысл выполнять оба обработчика во время действия предмета на предмет.

Ещё один пример, когда удобно вернуть false:

use = function(s, w)
	if w^'яблоко' then
		p [[Я почистил яблоко.]]
		w.cut = true
		return
	end
	return false;
end

В данном случае use у яблока обрабатывает только одну ситуацию -- действие на яблоко. В остальных случаях, обработчик возвращает false и движок вызовет метод по умолчанию: game.use.

Но лучше, если вы пропишете действие по умолчанию для ножа:

use = function(s, w)
	if w^'яблоко' then
		p [[Я почистил яблоко.]]
		w.cut = true
		return
	end
	p [[Не стоит размахивать ножом!]]
end

Этот пример также демонстрирует тот факт, что вторым параметром у use является предмет, на который мы действуем. У метода 'used', соответственно, второй параметр -- это объект, который действует на нас:

obj {
	nam = 'мусорка';
	dsc = [[В углу стоит {мусорка}.]];
	used = function(s, w)
		if w^'яблоко' then
			p [[Я выбросил яблоко в мусорку.]]
			remove(w)
			return
		end
		return false;
	end
}

Как вы помните, перед вызовом use вызывается обработчик onuse у объекта game, потом у объекта 'игрок', а потом у текущей комнаты. Вы можете блокировать 'use', вернув из любого из перечисленных методов 'onuse' -- false.

Использовать 'use' или 'used' (или оба) это вопрос личных предпочтений, однако, метод used вызывается раньше и, следовательно, имеет больший приоритет.

Объект "Игрок"

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

Имя этого объекта -- 'player'. Существует переменная-ссылка pl, которая указывает на этот объект.

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

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

game.player = player {
	nam = "Василий";
	room = 'кухня'; -- стартовая комната игрока
	power = 100;
	obj = { 'яблоко' }; -- заодно добавим яблоко в инвентарь
};

В INSTEAD есть возможность создавать нескольких игроков и переключаться между ними. Для этого служит функция 'change_pl()'. В качестве параметра передайте функции требуемый объект типа 'player' (или его имя). Функция переключит текущего игрока, и при необходимости, осуществит переход в комнату, где находится новый игрок.

Функция 'me()' всегда возвращает текущего игрока. Следовательно, в большинстве игр me() == pl.

Объект "Мир"

Игровой мир представлен объектом типа world. Имя такого объекта 'game'. Существует ссылка-переменная, которая также называется game.

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

Например, переменная game.codepage содержит кодировку исходного кода игры, и по умолчанию равна "UTF-8". Я не рекомендую использовать другие кодировки, но иногда, выбор кодировки может стать необходимостью.

Переменная game.player -- содержит текущего игрока.

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

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

Конечно, все они могут быть функциями.

Также, объект game может содержать обработчики: onact, ontak, onuse, oninv, onwalk -- которые могут прерывать действия, в случае возврата false.

Ещё у объекта game можно задать обработчики: afteract, afterinv, afteruse, afterwalk -- которые вызываются в случае успешного выполнения соответствующего действия.

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

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

room {
	nam = 'холодильник';
	frost = std.list { 'мороженное' };
}

Хотя, обычно, это не требуется. Ниже перечислены методы объектов типа 'список'. Вы можете вызывать их для любых списков, хотя обычно это будут way и obj, например:

ways():disable() -- отключить все переходы
  • disable() - отключает все объекты списка;
  • enable() - включает все объекты списка;
  • close() - закрыть все объекты списка;
  • open() - открыть все объекты списка;
  • add(объект|имя, [позиция]) - добавить объект;
  • for_each(функция, аргументы) - вызвать для каждого объекта функцию с аргументами;
  • lookup(имя/тег или объект) - поиск объекта в списке. Возвращает объект и индекс;
  • srch(имя/тег или объект) - поиск видимого объекта в списке;
  • empty() - вернёт true, если список пуст;
  • zap() - очистить список;
  • replace(что, на что) - заменить объект в списке;
  • cat(список, [позиция]) - добавить содержимое списка в текущий список по позиции;
  • del(имя/объект) - удалить объект из списка.

Существуют функции, возвращающие объекты-списки:

  • inv([игрок]) - вернуть инвентарь игрока;
  • objs([комната]) - вернуть объекты комнаты;
  • ways([комната]) - вернуть переходы комнаты.

Конечно, вы можете обращаться к спискам и напрямую:

	pl.obj:add 'нож'

Объекты в списках хранятся в том порядке, в котором вы их добавите. Однако, если у объекта присутствует числовой атрибут pri, то он играет роль приоритета в списке. Если pri не задан, значением приоритета считается 0. Таким образом, если вы хотите, чтобы какой-то объект был первым в списке, давайте ему приоритет pri < 0. Если в конце списка -- > 0.

obj {
	pri = -100;
	nam = 'штука';
	disp = 'Очень важный предмет инвентаря';
	inv = [[Осторожней с этим предметом.]];
}

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

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

  • в символах [ ] описаны необязательные параметры;
  • 'что' или 'где' - означает объект (в том числе комнату), заданный тегом, именем или переменной-ссылкой;

Итак, основные функции:

  • '_(что)' - получить объект;
  • 'me()' возвращает текущего объекта-игрока;
  • 'here()' возвращает текущую сцену;
  • 'where(что)' возвращает комнату или объект, в котором находится заданный объект. Если объект находится в нескольких местах, то можно передать второй параметр -- таблицу Lua, в которую будут добавлены эти объекты;
  • 'inroom(что)' аналогично where(), но вернёт комнату, в которой расположен объект (это важно для объектов в объектах);
  • 'from([где])' возвращает прошлую комнату, из которой игрок перешёл в заданную комнату. Необязательный параметр -- получить прошлую комнату не для текущей комнаты, а для заданной;
  • 'seen(что, [где])' возвращает объект или переход, если он присутствует и видим, есть второй необязательный параметр -- выбрать сцену или объект/список в котором искать;
  • 'lookup(что, [где])' возвращает объект или переход, если он существует в сцене или объекте/списке;
  • 'inspect(что)' возвращает объект, если он виден/доступен на сцене. Поиск производится по переходам и объектам, в том числе, в объектах игрока;
  • 'have(что)' возвращает объект, если он есть в инвентаре и не отключён;
  • 'live(что)' возвращает объект, если он присутствует среди живых объектов (описано далее);

Эти функции в основном используются в условиях, либо для поиска объекта с последующей модификацией. Например, вы можете использовать 'seen' для написания условия:

onexit = function(s)
	if seen 'монстр' then -- если у функции 1 параметр,
		--- скобки писать не обязательно
		p 'Монстр загораживает проход!'
		return false
	end
end

А также, для нахождения объекта в сцене:

use = function(s, w)
	if w^'окно' then
		local ww = lookup 'собака'
		if not ww then
			p [[А где моя собака?]]
			return
		end
		place(ww, 'улица')
		p 'Я разбил окно! Моя собака выпрыгнула на улицу.'
		return
	end
	return false
end

Пример с функцией 'have':

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

Может возникнуть вопрос, в чём разница между функциями lookup и _ ()? Дело в том, что lookup() ищет объект, и в случае, если объект не найден -- просто ничего не вернёт. А запись _ () предполагает, что вы точно знаете, что за предмет вы получаете. Другими словами, _ () это безусловное получение объекта по имени. Эта функция в общем случае не занимается поиском. Только если в качестве параметра задан тег, будет осуществлён поиск среди доступных объектов. Если вы используете _ () на несуществующий объект или недоступный тег -- вы получите ошибку!

Другие функции стандартной библиотеки

В INSTEAD в модуле stdlib, который всегда подключается автоматически, определены функции, которые предлагаются автору как основной рабочий инструмент по работе с миром игры. Рассмотрим их в этой главе.

При описании функции в большинстве функций под параметром 'w' понимается объект или комната, заданная именем, тегом или по переменной-ссылке. [ wh ] - означает необязательный параметр.

  • include(файл) - включить файл в игру;

      include "lib" -- включит файл lib.lua из текущего каталога с игрой;
    
  • loadmod(модуль) - подключить модуль игры;

      loadmod "module" -- включит модуль module.lua из текущего каталога;
    
  • rnd(m) - случайное целочисленное значение от '1' до 'm';

  • rnd(a, b) - случайное целочисленное значение от 'a' до 'b', где 'a' и 'b' целые >= 0;

  • rnd_seed(что) - задать зерно генератора случайных чисел;

  • p(...) - вывод строки в буфер обработчика/атрибута (с пробелом в конце);

  • pr(...) - вывод строки в буфер обработчика/атрибута "как есть";

  • pn(...) - вывод строки в буфер обработчика/атрибута (с переводом строки в конце);

  • pf(fmt, ...) - вывод форматной строки в буфер обработчика/атрибута;

      local text = 'hello';
      pf("Строка: %q Число: %d\n", text, 10);
    
  • pfn(...)(...)... "строка" - формирование простого обработчика; Данная функция упрощает создание простых обработчиков:

      act = pfn(walk, 'ванная') "Я решил зайти в ванную.";
      act = pfn(enable, '#переход') "Я заметил отверстие в стене!";
    
  • obj {} - создание объекта;

  • stat {} - создание статуса;

  • room {} - создание комнаты;

  • menu {} - создание меню;

  • dlg {} - создание диалога;

  • me() - возвращает текущего игрока;

  • here() - возвращает текущую сцену;

  • from([w]) - возвращает комнату из которой осуществлён переход в текущую сцену;

  • new(конструктор, аргументы) - создание нового динамического объекта (будет описано далее);

  • delete(w) - удаление динамического объекта;

  • gamefile(файл, [сбросить состояние?]) - подгрузить динамически файл с игрой;

      gamefile("part2.lua", true) -- сбросить состояние игры (удалить
      объекты и переменные), подгрузить part2.lua и начать с main комнаты.
    
  • player {} - создать игрока;

  • dprint(...) - отладочный вывод;

  • visits([w]) - число визитов в данную комнату (или 0, если визитов не было);

  • visited([w]) - число визитов в комнату или false, если визитов не было;

      if not visited() then
      	p [[Я тут первый раз.]]
      end
    
  • walk(w, [булевое exit], [булевое enter], [булевое менять from]) - переход в сцену;

      walk('конец', false, false) -- безусловный переход (игнорировать
      onexit/onenter/exit/enter);
    
  • walkin(w) - переход в под-сцену (без вызова exit/onexit текущей комнаты);

  • walkout([w], [dofrom]) - возврат из подсцены (без вызова enter/onenter);

  • walkback([w]) - синоним walkout([w], false);

  • _(w) - получение объекта;

  • for_all(fn, ...) - выполнить функцию для всех аргументов;

      for_all(enable, 'окно', 'дверь');
    
  • seen(w, [где]) - поиск видимого объекта;

  • lookup(w, [где]) - поиск объекта;

  • ways([где]) - получить список переходов;

  • objs([где]) - получить список объектов;

  • search(w) - поиск доступного игроку объекта;

  • have(w) - поиск предмета в инвентаре;

  • inroom(w) - возврат комнаты/комнат, в которой находится объект;

  • where(w, [таблица]) - возврат объекта/объектов, в котором находится объект;

      local list = {}
      local w = where('яблоко', list)
      -- если яблоко находится в более, чем одном месте, то
      -- list будет содержать массив этих мест.
      -- Если вам достаточно одного местоположения, то:
      where 'яблоко' -- будет достаточно
    
  • closed(w) - true если объект закрыт;

  • disabled(w) - true если объект выключен;

  • enable(w) - включить объект;

  • disable(w) - выключить объект;

  • open(w) - открыть объект;

  • close(w) - закрыть объект;

  • actions(w, строка, [значение]) - возвращает (или устанавливает) число действий типа t для объекта w.

      if actions(w, 'tak') > 0 then -- предмет w был взят хотя бы 1 раз;
      if actions(w) == 1 then -- act у предмета w был вызван 1 раз;
    
  • pop(тег) - возврат в прошлую ветвь диалога;

  • push(тег) - переход в следующую ветвь диалога

  • empty([w]) - пуста ли ветвь диалога? (или объект)

  • lifeon(w) - добавить объект в список живых;

  • lifeoff(w) - убрать объект из списка живых;

  • live(w) - объект жив?;

  • change_pl(w) - смена игрока;

  • player_moved([pl]) - текущий игрок перемещался в этом такте?;

  • inv([pl]) - получить список-инвентарь;

  • remove(w, [wh]) - удалить объект из объекта или комнаты; Удаляет объект из списков obj и way (оставляя во всех остальных, например, game.lifes);

  • purge(w) - уничтожить объект (из всех списков); удаляет объект из всех списков, в которых он присутствует;

  • replace(w, ww, [wh]) - заменить один объект на другой;

  • place(w, [wh]) - поместить объект в объект/комнату (удалив его из старого объекта/комнаты);

  • put(w, [wh]) - поместить объект без удаления из старого местоположения;

  • take(w) - забрать объект;

  • drop(w, [wh]) - выбросить объект;

  • path {} - создать переход;

  • time() - число ходов от начала игры.

Важно!

На самом деле, многие из этих функций также умеют работать не только с комнатами и объектами, но и со списками. То есть 'remove(apple, inv())' сработает также как и 'remove(apple, me())''; Впрочем, remove(apple) тоже сработает и удалит объект из тех мест, где он присутствует.

Рассмотрим несколько примеров.

act = function()
	pn "Я иду в следующую комнату..."
	walk (nextroom);
end

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

Важно!

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

act = function()
        pn "Я иду в следующую комнату..."
        walk (nextroom);
        return
end

Не забывайте также, что при вызове 'walk' вызовутся обработчики 'onexit/onenter/exit/enter'' и если они запрещают переход, то он не произойдёт.

Диалоги

Диалоги -- это сцены специального типа 'dlg', содержащие объекты -- фразы. При входе в диалог игрок видит перечень фраз, которые может выбирать, получая какую-то реакцию игры. По умолчанию, уже выбранные фразы скрываются. При исчерпании всех вариантов, диалог завершается выходом в предыдущую комнату (конечно, если в диалоге нет постоянно видимых фраз, среди которых обычно встречается что-то типа 'Завершить разговор' или 'Спросить ещё раз'). При повторном входе в диалог, все скрытые фразы снова становятся видимыми и диалог сбрасывается в начальное состояние (если, конечно, автор игры специально не прикладывал усилия по изменению вида диалога).

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

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

Хотя я рекомендую использовать 'walkin', так как в случае 'walkin' не вызываются 'onexit/exit' текущей комнаты, а персонаж, с которым мы можем поговорить, обычно находиться в этой же комнате, где и главный герой. То есть:

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

Если вам не нравится префикс у фраз в виде дефиса, вы можете определить строковую переменную:

std.phrase_prefix = '+';

И получить префикс в виде '+' перед каждой фразой. Вы также можете сделать префикс функцией. На вход функции в таком случае будет поступать в виде параметра номер фразы. Задача функции -- вернуть строковый префикс.

Обратите внимание, что 'std.phrase_prefix' не сохраняется, если вам нужно переопределять её на лету, вам придётся восстанавливать её состояние в 'start()' функции вручную!

Важно!

Я рекомендую использовать модуль 'noinv' и задавать свойство 'noinv' в диалогах. Диалоги будут выглядеть красивей, и вы обезопасите свою игру от ошибок и непредсказуемых реакций, при использовании инвентаря внутри диалога (так как обычно автор не подразумевает такие вещи). Например:

require "noinv"
...
dlg {
        nam = 'Охранник';
        -- в диалогах обычно не нужен инвентарь
        noinv = true;
        ...
}

Фразы

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

dlg {
	nam = 'разговор';
	title = [[Разговор с продавцом]];
	enter = [[Я обратился к продавцу.]];
	phr = {
		{ 'У вас есть бобы?', '-- Нет.'},
		{ 'У вас есть шоколад?', '-- Нет.'},
		{ 'У вас есть квас?', '-- Да',
			{ 'А сколько он стоит?', '-- 50 рублей.' },
			{ 'А он холодный?', '-- Холодильник сломался.',
				{ 'Беру два!', 'Остался один.',
					{ 'Дайте один!', function() p [[Ок!]]; take 'квас'; end };
				}
			}
		}
	}
}

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

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

Пара может быть простой:

{'Вопрос', 'Ответ }

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

{'Вопрос', 'Ответ',
	{'Под-вопрос1', 'Под-ответ1' },
	{'Под-вопрос2', 'Под-ответ2' },
}

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

dlg {
	nam = 'разговор';
	title = [[Разговор с продавцом]];
	enter = [[Я обратился к продавцу.]];
	phr = {
	-- тут мог бы быть вопрос ответ 1-го уровня!
	-- 'Главный вопрос', 'Главный ответ',
		{ 'У вас есть бобы?', '-- Нет.'},
		{ 'У вас есть шоколад?', '-- Нет.'},
		{ 'У вас есть квас?', '-- Да',
			{ 'А сколько он стоит?', '-- 50 рублей.' },
			{ 'А он холодный?', '-- Холодильник сломался.',
				{ 'Беру два!', 'Остался один.',
					{ 'Дайте один!', function() p [[Ок!]]; take 'квас'; end };
				}
			}
		}
	}
}

На самом деле, так и есть. И вы можете добавить 'Главный вопрос' и 'Главный ответ', но только вы не увидите этот главный вопрос. Дело в том, что при входе в диалог фраза phr автоматически раскрывается, так как обычно нет никакого смысла в диалогах из одной единственной фразы. И гораздо проще понять диалог как набор выборов, чем как единственную древовидную фразу. Так что у phr никогда нет первоначальной пары вопрос-ответ, но мы сразу попадаем в массив вариантов, что более понятно.

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

dlg {
	nam = 'разговор';
	title = [[Разговор с продавцом]];
	enter = [[Я обратился к продавцу.]];
	phr = { -- это объект типа фраза, без dsc и act
		-- это 1-я фраза, внутри фразы с dsc и act
		{ 'У вас есть бобы?', '-- Нет.'},
		-- это 2-я фраза, внутри фразы с dsc и act
		{ 'У вас есть шоколад?', '-- Нет.'},
		-- это 3-я фраза, внутри фразы с dsc и act
		{ 'У вас есть квас?', '-- Да',
		-- это 1-я фраза внутри 3й фразы с dsc и act
		{ 'А сколько он стоит?', '-- 50 рублей.' },
			{ 'А он холодный?', '-- Холодильник сломался.',
				{ 'Беру два!', 'Остался один.',
				    -- здесь act в виде функции
					{ 'Дайте один!', function() p [[Ок!]]; take 'квас'; end };
				}
			}
		}
	}
}

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

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

std.phrase_show = false -- не выводить фразу-вопрос при выборе

Эта настройка действует на все диалоги, устанавливайте её в init() или start() функции.

Атрибуты фраз

Рассмотрим вариант фразы:

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
	}
}

Если запустить этот диалог, то после выбора, скажем, красной таблетки, у нас останется ещё один выбор синей таблетки. Но наш замысел, явно не в этом! Существует несколько способов сделать диалог правильным.

Во первых, вы можете воспользоваться pop() -- возвратом на предыдущий уровень диалога:

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		{'Красную', function() p 'Держите!'; pop() end; },
		{'Синюю', function() p 'Вот!'; pop() end; },
	}
}

Или, в другой записи:

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		{'Красную', pfn(pop) 'Держите!' },
		{'Синюю', pfn(pop) 'Вот!' },
	}
}

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

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
	}
}

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

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

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
		{ true, 'А какая лучше?', 'Тебе выбирать.' }, -- фраза
		-- которая никогда не будет скрыта
	}
}

Альтернативная запись, с явным заданием атрибута always:

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
		{ always = true, 'А какая лучше?', 'Тебе выбирать.' }, -- фраза
		-- которая никогда не будет скрыта
	}
}

Ещё один пример. Что-если мы хотим, чтобы фраза была показана (или спрятана) по какому-либо условию? Для этого есть функция-обработчик cond.

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
		{ true, 'А какая лучше?', 'Тебе выбирать.' }, -- фраза
		-- которая никогда не будет скрыта
	},
	{ cond = function() return have 'яблоко' end,
		'А хотите яблоко?', 'Спасибо, нет.' };
}

В данном примере, только при наличии у игрока яблока, покажется ветка диалога 'А хотите яблоко?'.

Иногда бывает удобно выполнить действие в тот момент, когда варианты текущего уровня(контекста) диалога исчерпаны. Для этого служит функция-обработчик onempty.

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
		onempty = function()
			p [[Ты сделал свой выбор.]]
			pop()
		end;
	},
	{ cond = function() return have 'яблоко' end,
		'А хотите яблоко?', 'Спасибо, нет.' };
}

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

Все описанные атрибуты могут быть установлены у любой фразы. В том числе и на 1-м уровне:

phr = {
	onempty = function()
		p [[Вот и поговорили.]]
		walkout()
	end;
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!' },
		{'Синюю', 'Вот!' },
		onempty = function()
			p [[Ты сделал свой выбор.]]
			pop()
		end;
	},
	{ cond = function() return have 'яблоко' end,
		'А хотите яблоко?', 'Спасибо, нет.' };
}

Теги

Только что мы рассмотрели механизмы диалогов, которые уже позволяют создавать довольно сложные диалоги. Однако, и этих средств может не хватить. Иногда нам нужно уметь обращаться к фразам из других мест диалога. Например, выборочно включать их, или анализировать их состояние. А также делать переходы из одних ветвей диалога в другие.

Всё это возможно для фраз, у которых есть тег. Создать фразу с тегом очень просто:

phr = {
	{ '#что?', 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		{'#красная', 'Красную', 'Держите!' },
		{'#синяя', 'Синюю', 'Вот!' },
	},
}

Как видим, наличие в начале фразы строки, которая начинается на символ '#' - означает наличие тега.

Для таких фраз работают стандартные методы, такие как seen или enable/disable. Например, мы могли бы обойтись без атрибута only следующим образом:

phr = {
	{ '#что?', 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		{'#красная', 'Красную', 'Держите!'
			cond = function(s)
				return not closed('#синяя')
			end
		},
		{'#синяя', 'Синюю', 'Вот!',
			cond = function(s)
				return not closed('#красная')
			end
		},
	},
}

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

push(куда) -- делает переход на фразу с запоминанием позиции в стеке.

pop([куда]) -- вызванная без параметра, поднимается на 1 позицию в стеке истории. Можно указать конкретный тег фразы, которая должна быть в истории, в таком случае возврат будет осуществлён на неё.

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

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!', next = '#отаблетке' },
		{ 'Синюю', 'Вот!', next = '#отаблетке' },
	},
	{ false, '#отаблетке',
		{'Я сделал верный выбор?', 'Время покажет.'}
	},
}

Тут мы видим сразу несколько приёмов:

  • атрибут next, вместо явного описания реакции в виде функции с push. next -- это простой способ записать push.

  • false в начале фразы, делает фразу выключенной. Она находится в состоянии выключена, пока не сделать явный enable. Однако внутрь фразы мы можем перейти, и показать содержимое выборов. Альтернативная запись возможна с использованием атрибута hidden:

	{ hidden = true, '#отаблетке',
		{'Я сделал верный выбор?', 'Время покажет.'}
	},

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

phr = {
	{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
		only = true,
		{'Красную', 'Держите!', next = '#отаблетке' },
		{ 'Синюю', 'Вот!', next = '#отаблетке' },
	},
	{ false, '#отаблетке', [[Я взял таблетку и мастер хитро улыбнулся.]],
		{'Я сделал верный выбор?', 'Время покажет.'},
		{'Что делать дальше?', 'Ты свободен.'},
	},
}

При выборе таблетки, будет вызван заголовочный метод фразы '#отаблетке', а уже потом будет представлен выбор.

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

dlg {
	nam = 'диалог';
	phr = {
		{ 'Что это у вас?', 'Таблетки. Красная и синяя. Вам какую?',
			only = true,
			{'Красную', 'Держите!', next = '#отаблетке' },
			{ 'Синюю', 'Вот!', next = '#отаблетке' },
		}
	}
}:with {
	{ '#отаблетке', [[Я взял таблетку и мастер хитро улыбнулся.]],
		{'Я сделал верный выбор?', 'Время покажет.'},
		{'Что делать дальше?', 'Ты свободен.'},
	},
}

Дело в том, что атрибут phr диалога задаёт первый объект комнаты. Но вы можете заполнить объекты комнаты обычным образом: задав obj или with. Так как при входе в диалог раскрывается 1-я фраза, то остальные фразы вы не увидите (обратите внимания, у фразы '#отаблетке' не стоит false), но вы сможете делать переходы на эти фразы.

Методы

Как вы уже знаете, объекты в INSTEAD могут находиться в состоянии открыт/закрыт и выключен/включён. Как это соответствует фразам диалога?

Для обычных фраз, после активации выбора фраза закрывается. При повторном входе в диалог все фразы открываются.

Для фраз с always = true (или true в начале определения) -- такого закрытия не происходит.

Для фраз с hidden = true (или false в начале определения) -- фраза будет создана как выключенная. Она не будет видима до тех пор, пока не будет явно включена.

Для фраз с cond(), каждый раз при просмотре фраз вызывается этот метод, и в зависимости от возвращаемого значения (true/не true) фраза включается или выключается.

Зная это поведение, вы можете прятать/показывать и анализировать фразы обычными функциями вида: disable / enable / empty / open / close / closed / disabled и так далее...

Однако, делать вы это можете только в самом диалоге, так как все фразы идентифицируются по тегам. Если вы хотите модифицировать состояние/анализировать фразы из других комнат вы можете:

  • дать фразе имя { nam = 'имя' }...
  • искать фразу по тегу в другой комнате: local ph = lookup('#тег', 'диалог') и потом работать с ней;

Что касается функций push/pop, то вы можете вызывать их явно как методы диалога, например:

	_'диалог':push '#новая'

Но лучше это делать в самом диалоге, например, в enter.

Кроме того, есть метод :reset, который сбрасывает стек переходов и устанавливает стартовую фразу, например:

enter = function(s)
	s:reset '#начало'
end

Следует отметить, что когда вы делаете enable/disable/open/close фразы, то вы выполняете действие именно над этой фразой, а не над фразами включёнными внутрь. Но так как при показе фраз движок остановится на выключенном/закрытом объекте-фразе и не войдёт внутрь, этого достаточно.

Специальные объекты

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

  1. Системные объекты @;
  2. Подстановки $.

Системные объекты, это объекты, чьё имя начинается с символа '@' или '$'. Такие объекты обычно создаются в модулях. Они не уничтожаются при смерти игрового мира (например, при подгрузке gamefile, при загрузке игры из сохранения, и так далее). Примеры объектов: @timer, @prefs, @snd.

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

Объект '@'

Обычно, вам не нужно работать с такими объектами, но в качестве примера рассмотрим реализацию 'ссылок'.

Пусть мы хотим сделать ссылку, при нажатии на которую мы перейдём в другую комнату. Конечно, мы могли бы добавить объект в сцену, но стоит ли это делать в таком простом случае?

Как нам может помочь системный объект?

obj {
	nam = '@walk';
	act = function(s, w)
		walk(w, false, false)
	end;
}
room {
	nam = 'main';
	title = 'Начало';
	decor = [[Начать {@walk старт|приключение}]];
}

При нажатии на ссылку "приключение" будет вызван метод act объекта '@walk' с параметром "старт".

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

xact.walk = walk

room {
	nam = 'main';
	title = 'Начало';
	decor = [[Начать {@ walk старт|приключение}]];
}

Обратите внимание, на пробел после @. Данная запись делает следующее:

  • берет объект '@' (такой объект создан библиотекой stdlib);
  • берет его act;
  • вызывает act с параметрами walk и старт;
  • act объекта '@' смотрит в массив xact;
  • walk определяет метод, который будет вызван из массива xact;
  • старт -- параметр этого метода.

Другой пример:

xact.myprint = function(w)
	p (w)
end

room {
	nam = 'main';
	title = 'Начало';
	decor = [[Нажми {@ myprint "hello world"|на кнопку}]];
}

Подстановки

Объекты, чьё имя начинается на символ '$' тоже считаются системными объектами, но работают они по-другому.

Если в выводе текста встречается "ссылка" вида:

{$my a b c|текст}

То происходит следующее:

  1. Берётся объект $my;
  2. Берётся act объекта $my;
  3. Вызывается act: _'$my':(a, b, c, текст);
  4. Возвращаемая строка заменяет собой всю конструкцию {...}.

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

Зачем это нужно? Представьте себе, что вы разработали модуль, который превращает записи формул из текстового вида в графические. Вы пишете объект $math который в своём act методе превращает текст в графическое изображение (спрайт) и возвращает его в текстовый поток. Тогда пользоваться таким модулем крайне просто, например:

	{$math|(2+3*x)/y^2}

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

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

  • Игрок нажимает на ссылку;
  • Реакция 'act', 'use'', 'inv', 'tak', осмотр сцены (клик по названию сцены) или переход в другую сцену;
  • Динамические события;
  • Вывод нового состояния сцены.

Например, сделаем Барсика живым:

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

И вот момент в игре, когда Барсик попадает к нам за пазуху!

take 'Барсик' -- добавить в инвентарь
lifeon 'Барсик' -- оживить Барсика!

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

Если в вашей игре много "живых" объектов, вы можете задавать им явную позицию в списке, при добавлении. Для этого, воспользуйтесь вторым числовым параметром (целое неотрицательное число) 'lifeon', чем меньше число, тем выше приоритет. 1 -- самый высокий. Или вы можете использовать атрибут pri у объекта. Правда, этот атрибут будет влиять на приоритет объекта в любом списке.

Если вам нужен фоновый процесс в какой-то комнате, запускайте его в 'enter' и удаляйте в 'exit', например:

room {
        nam  = 'В подвале';
        dsc = [[Тут темно!]];
        enter = function(s)
                lifeon(s);
        end;
        exit = function(s)
                lifeoff(s);
        end;
        life = function(s)
                if rnd(10) > 8 then
                        p [[Я слышу какие-то шорохи!]];
                        -- изредка пугать игрока шорохами
                end
        end;
        way =  { 'Дом' };
}

Если вам нужно определить, был ли переход игрока из одной сцены в другую, воспользуйтесь 'player_moved()'.

obj {
	nam  = 'фонарик';
	on = false;
	life = function(s)
		if player_moved() then -- гасить фонарик при переходах
			s.on = false
			p "Я выключил фонарик."
			return
		end;
	end;
...
}

Для отслеживания протекающих во времени событий, используйте 'time()' или вспомогательную переменную-счётчик. Для определения местоположения игрока -- 'here()'. Для определения факта, что объект "живой" -- 'live()'.

obj {
	nam  = 'динамит';
	timer = 0;
	used = function(s, w)
		if w^'спичка' then -- спичка?
			if live(s) then
				return "Уже горит!"
			end
			p "Я поджёг динамит."
			lifeon(s)
			return
		end
		return false -- если не спичка
	end;
	life = function(s)
		s.timer = s.timer + 1
		if s.timer == 5 then
			lifeoff(s)
			if here() == where(s) then
				p [[Динамит взорвался рядом со мной!]]
			else
				p [[Я услышал, как взорвался динамит.]];
			end
		end
	end;
...
}

Если 'life' обработчик возвращает текст события, он печатается после описания сцены.

Вы можете вернуть из обработчика 'life' второй код возврата, ('true' или 'false'). Если вы вернёте true, то это будет признаком важного события, которое выведется до описания объектов сцены, например:

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

Или:

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

Если вы вернёте false, то цепочка life методов прервётся на вас. Это удобно делать при выполнении walk из метода life, например:

life = function()
	walk 'theend'
	return false -- это последний life
end

Если вы хотите блокировать 'life' обработчики в какой-то из комнат, воспользуйтесь модулем 'nolife'. Например:

require "noinv"
require "nolife"

dlg {
        nam = 'Охранник';
        noinv = true;
        nolife = true;
...
}

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