The engine for INSTEAD is written in Lua 5.1 and 5.2, therefore knowing this language is useful, although completely unnecessary. In fact, it was the express intention of the creator that new designers should be able to take on interesting projects without any need for engine-level programming.
Throughout its evolution, the engine's capabilities have widened and allowed implementation of games in various genres including arcade games and highly complex text-parsing adventures. It's even possible to launch games written for other engines. However, the original intent was custom text-based adventures, and this remains the primary focus. This is what the present documentation describes, and strengthening these foundation skills is important even if you plan to write a genre-bending game in the future, you clever child. So let's start learning INSTEAD by writing a simple text adventure!
Most people associate a text adventure with some text printed on the screen and a few pre-defined courses of action, for example:
You see a table in front of you. There is an apple on it. What do you do? 1) Take the apple 2) Step away from the table
Some classic games have the player type her actions literally, like this:
You are in the kitchen. There is a table near a window. PLAYER TYPES> look at the table There is an apple on the table.
Both of these implementations have advantages as well as drawbacks. The first approach resembles what's known as the gamebook genre and doesn't suit classic text adventures where a hero could explore a virtual world, free to move and interact with in-world objects. The second approach gives us a more realistic model, but requires more effort from the game developer and her players, making accommodation for human language with complex grammar.
The INSTEAD game engine was intended for games which combine the advantages of both approaches, at the same time attempting to avoid their drawbacks. The game world in INSTEAD is modeled in a similar fashion to the second approach in that there are places (rooms) which the protagonist can visit, and objects (including NPC characters) she can interact with. The player explores the world and can manipulate objects freely. Acting upon objects might remind her of classical graphic quests from the 1990s, yet with a greater depth of features.
Before reading this manual it's recommended you play some INSTEAD games to get an idea how each concept contained herein is implemented. However, it must be noted many old games are written with less than optimal code using deprecated constructions, so studying their code is a treacherous endeavor for the fledgling game developer. Modern structure and syntax enables you to write more laconically, simply, and clearly, so go have fun with some games then return here to pick up best practices.
The game window displays text and images for the room the player is standing in.
The static elements of a room are shown only once, upon entering. Clicking on the room's title will recall them.
The dynamic elements of a room are defined by its objects, including character dialogues. These are always visible to the player, yet can change based on the player's actions.
The player's inventory is represented in a separate panel that contains interactive objects the player can access throughout the game.
Player actions in the game world include:
A game should have its own primary directory within your system directory, with one main.lua
script inside the top level. Other game resources should be placed in this directory as well, and can be organized in sub-directories. This includes Lua scripts, image files, sound files, and savegame files. All references in the code must be made relative to this top level directory.
To save yourself having to search for save files elsewhere, be sure to create a sub-directory named “saves”. Your saved games will automatically be stored here.
At the beginning of the main.lua
file a header can be defined with information tags. Each tag should start with – &
and end with a closing $
symbol.
The $Name
tag contains the game title, encoded in UTF-8 without BOM!
, which will be displayed by the interpreter when the player opens the game:
-- $Name: The most interesting game!$
It's also recommended to specify the development version of the game:
-- $Version: 0.5$
As well as the author(s):
-- $Author: Non Anonymous$
If you develop your game in Windows make sure your editor supports UTF-8 encoding without BOM!
and use it to write the code.
Since version 1.2.0
you must define the STEAD API version after the header. It's currently version 1.9.1
as of this writing.
instead_version "1.9.1"
Without this line STEAD API will operate in compatibility/legacy mode. The game engine has competent backwards compatibility, but by explicitly specifying a modern API version number it's possible to use features such as snapshots, autosaved global variables, on-the-fly functions, auto-formatting, feature modules, and so on. There's no point writing new games in an old API, although such practice is common. For this reason, it's best NOT to attack game code until you've studied this manual. It will result in a better game.
Modules are included beneath the API declaration:
require "para" --different indents require "dash" --replaces double hyphen sign with a large hyphen require "quotes" --replaces quotation marks with chevrons << >>
It's usually a good idea to define default handlers here as well:
-- an impossible attempted action generates -- this response by default game.act = 'Does not work.'; game.use = 'It will not help.'; game.inv = 'Do I really need this?';
Game initialization should be defined in the init function which will be called by the engine at the very beginning. It's a good place to set the player initial state or similar early requirements for the player. However you may not need an init
function.
function init() -- set a savegame stored variable me()._know_truth = false; -- add knife to the player's inventory take(knife); -- add paper to the player's inventory take(paper); end;
The INSTEAD installation directory is where the graphic interpreter looks for available games. The Unix version also checks ~/.instead/games
. The Windows version since 0.8.7
checks Documents_and_Settings/USER/Local_Settings/Application_Data/instead/games
but from STEAD 1.2.0
on both Windows and Unix standalone builds looks at ./appdata/games
, if it exists.
In some INSTEAD builds (on Windows or on Linux if the project is built with gtk, etc.) it's possible to open a game from any location using the menu option “Select Game” or by hitting F4. If there's only one game in the directory it will be launched by the interpreter automatically, which is handy if you distribute your game with the engine included. Your players can simply launch INSTEAD to start their adventure.
While writing code it's strongly recommended to use indents for nested levels like in the code samples in this manual. It really helps to clarify and decreases mistakes.
Here's a basic template for your first game:
-- $Name: My first game$ -- $Version: 0.1$ -- $Author: Homo codius$ instead_version "1.9.1" require "para" --for decoration require "dash" require "quotes" require "dbg" --for debugging game.act = 'Um...'; game.use = 'It does not works!'; game.inv = 'Why?'; function init() --some initialization code if needed end
In the event bugs occur (they will), it's handy to run INSTEAD with the command line parameter -debug
which will generate more informative error messages. The -debug
parameter can also be specified in the desktop icon/shortcut properties. Right click your INSTEAD desktop icon, select Properties from the dialog menu, then type -debug
at the end of the Target:
field.
In Windows, debug mode opens a separate console window where errors are output. Unix uses the console where INSTEAD was launched from.
Also you can engage an internal hotkey debugger module (F7) by adding the following line after instead_version
in the header of the main.lua
file.
require "dbg"
Debug mode displays the call stack, as well as allows you to quickly restart the game with Alt+R. Combining this hotkey with quicksave/quickload (F8/F9) it's possible to assess changes very efficiently with each edit you write to your code.
In your code you can force output your own custom debug messages using the print()
method inside a function placed within an object's act
handler:
act = function(s) print ("Act is here! "..stead.deref(s)); ... end;
Don't fear the previous example! After reading this manual, and with your own game on the anvil, you'll look at this code with more inspiration.
HINT: It's often useful to examine savegame files during debugging because they contain special stored game state variables.
If you're already an experienced debugger you'd probably like the ability to check your Lua syntax by parsing it with the luac
compiler. In Unix just use the -p
option at the command line.
On Windows you'll have to install the Lua binaries from here , then run luac52.exe
to parse your code.
You can also run a syntax check with your initial program call, as long as you're running version 1.9.2
or higher. Just add -luac
to your call.
sdl-instead -debug -luac <path/to/lua/script.lua>
A room is a basic game unit in which the player can exist and interact with objects. A room can represent a literal room inside a house or a massive redwood forest.
Every game must contain one variable called main
of object type room
. This main room is always where the game begins and where the player appears upon starting the game.
HINT: It could also be a custom start menu.
main = room { nam = "The Main Room"; dsc = [[You are in the first room of this game.]]; }
The definition above, signified by the =
sign, actually creates an object in the game world, a room
simply being a special kind of object. Most things in INSTEAD are objects.
For every object there are two requisite attributes, nam
and dsc
, which are closed with a semicolon or a comma.
Many more attributes and handlers can come along with these two. They can receive text strings, numbers, booleans, and even functions.
The above example's nam
attribute contains a text string that will be displayed at the top of the game window. The nam
attribute can be used to target the object from other code. You can often refer to an object by its nam
rather than its variable, in this case main
, when developing more complex interactions throughout your game.
In fact, nam
doesn't have to be output to the screen at all, to be reserved for other uses. The disp
attribute, if present, will override it in the graphical interpreter and be displayed at the top of the player's window.
main = room { nam = 'Start'; -- displayed in the window disp = 'My old room, covered in dust...'; dsc = [[I'd almost forgotten the color of the walls.]]; }
This provides you the developer with multiple names by which you can target an object.
The dsc
attribute contains a static description of the object, which gets displayed to the player on her first entrance. To display dsc
again she must look at the room explicitly, or if it's your intention for dsc
to be shown repeatedly, you can include the forcedsc
attribute.
Either put it with your other game
definitions at the top of the main.lua
file.
game.forcedsc = true;
Or inside the room's attribute list.
main = room { forcedsc = true; nam = 'The main room'; dsc = [[Guess what.]]; };
However, it's not recommended to use this technique because the engine is currently optimized for classic games to which it can't apply.
You can also use stead.need_scene()
to force display static dsc
in current room wherever you need. Read more in the Auxiliary Methods/Functions section.
A string can be enclosed with single or double quotation marks.
main = room { nam = 'The very same room'; dsc = "Got it in one!"; };
For long text strings it's better to use double-bracket notation.
dsc = [[ Very loooong description... ]];
Line breaks are ignored, so you must use the ^
symbol if you want to display one on the player's screen.
dsc = [[ First paragraph. ^^ Second one.^^ The third.^ New line here:)]];
Typical passages look like links at the top of the main story panel in the player's game window. These connections are defined in the way
attribute as a list. Rooms are listed by nam
reference (or using a variable identifier, as long the room has been defined earlier in the code). This is written the same as an obj
list.
main = room { nam = 'main room', dsc = 'You are in a large room.', obj = { 'tabl' }, way = { 'room2' }, }; room2 = room { nam = 'hall', dsc = 'You are in a huge hall.', way = { 'main' }, };
Two rooms are defined here. It's useful to remember that nam
and disp
can be written as functions to utilize dynamic room captions, which are generated on the fly, for example in a room whose nam
shouldn't be known to the player until he enters. However, there are more appropriate tools for this purpose such as the wroom
module discussed later.
When passing between rooms the engine calls the exit handler of the current room and the enter handler of the destination:
hall = room { enter = 'You enter the hall.', nam = 'Hall', dsc = 'This is one gigantically stupendous hall.', way = { 'main' }, exit = 'You leave the hall.', };
Like any handler, exit and enter may be defined as functions. The first parameter is the current room. The second parameter is the previous room f
for entering, or the destination room t
for exiting:
hall = room { enter = function(s, f) if f == main then p 'You came from the room.'; end end, nam = 'hall', dsc = 'You are in a huge hall.', way = { 'main' }, exit = function(s, t) if t == main then p 'I don\'t wanna go back!' return false end end, };
As we see, the handlers can return two values, a string for graphical display and/or a boolean for changing a status. In our example the exit
handler's function returns false
if the player tries to go into the main room from the hall. It means she'll have to find another way, like the Barlog :). The same logic can be applied to the enter
and tak
handlers.
You can also return both values in a more concise way:
-- action text followed by comma separating the status value return "I don't wanna go back.", false
Take into account the room pointer here()
may not update in sync with the enter
call! You can instead use the left
and entered
handlers which are triggered after the player has passed through. These handlers are recommended if you don't need to forbid passage.
Sometimes you'll want to give a passage a clever title that's distinct from the destination room's nam
. There are several ways to implement this, but the easiest is by using the vroom
module:
hall = room { nam = 'hall'; dsc = 'You are in the hall'; -- the syntax for vroom is ('passage name', 'destination room') way = { vroom('To the first room', 'main') }; } main = room { nam = 'main room'; dsc = 'You are in the small room.'; obj = { 'tabl' }; way = { vroom('To the hall', hall) }; };
Technically the vroom
module returns an auxiliary room
object with a special enter
handler set to bounce the player into the destination room. Yippee!
Sometimes you may need to get a bit more complex by disabling and enabling passages. The engine's concept of passages assumes they are always visible even if blocked, such as a room within a house with a locked door. Therefore it's unnecessary to hide passages.
Instead you can check for a key in the player's inventory, which will be implemented in the enter
handler of the destination room. If no key is present, a notification can alert the player and forbid her passing through. You can also create a door as a room
object, place it inside another room, and implement unlocking it with a key, to allow the player to pass using the customary way
list.
There are situations when you won't want a passage to be obvious to the player, or to make it appear as the result of a special event. For example a clock might be hiding a secret tunnel behind it:
hall = room { nam = 'hall'; dsc = 'You are in a huge hall.'; obj = { 'clock' }; way = { vroom('Behind the clock', 'behindclock'):disable() }; }; clock = obj { nam = 'clock'; dsc = [[You see an old {clock}.]]; act = function(s) --the path() method employs the enable method path('Behind the clock'):enable() p [[You discover a secret tunnel behind the clock!]]; end; };
So as you can see, an initially hidden passage is created with the vroom()
and :disable()
methods. You can easily hide objects in lists this fashion, so behindclock
can disappear from the room description, but it will still be available to be called later. A useful technique for hidden objects is to initialize them in a disabled state like this. The object isn't processed inside the engine until the :enable()
method has returned it.
Semantically, you can also write these enable/disable
methods reversed like this:
way = { disable(vroom('In the hours, 'inclock')) };
Then inside the clock object, its act handler invokes the path()
method, which references the 'Behind the clock' object vroom
aliased just prior, and calls enable()
on it. You can also write it this way:
act = function(s) enable(path('Behind the clock')) --or, since you don't have to parenthesize solo -- parameters in Lua, you could also write it like this: -- enable(path 'Behind the clock') p [[You discover a secret tunnel behind the clock!]]; end;
If the passage we want to toggle on or off is in another room, we can pass it as the second parameter of path()
.
path('Behind the clock', 'room312'):enable();
If you'd rather create simple passage variables, you can define them with vroom
inside like this:
path_clock = vroom('Behind the clock', 'behindclock'); clock = obj { nam = 'clock'; dsc = [[You see an old {clock}.]]; act = function(s) path_clock:enable() p [[You discover a secret tunnel behind the clock!]]; end; } hall = room { nam = 'hall'; dsc = 'You are in a huge hall'; obj = { 'clock' }; way = { path_clock:disable() }; };
You can also toggle a room object itself without employing vroom
.
inclock = room { nam = 'In the tunnel'; dsc = [[It's dark here.]]; }:disable(); --simply tacks on a disable -- method to the end of the room object -- the following three methods are also equivalent: -- }:disable() -- }; inclock:disable() -- }; disable(inclock) clock = obj { nam = 'clock'; dsc = [[You see an old {clock}.]]; act = function(s) -- toggles the "inclock" room to true inclock:enable() p [[You discover a secret tunnel behind the clock!]]; end; }; hall = room { nam = 'hall'; dsc = 'You are in a huge hall'; obj = { 'clock' }; way = { 'inclock' }; };
Objects are what the player interacts with. Here are two examples:
tabl = obj { --you can't name it table with an 'e' because -- it's a Lua keyword. Be careful with keywords and variables. nam = 'table', disp = 'Table', dsc = 'There\'s a {table} in the room.', -- you can escape -- apostrophes using \ so they -- won't interfere with the string act = 'Hm... just a table...', -- displays when -- object's hotlink is clicked in the dsc }; salt = obj { -- this variable can be used -- inside the obj list of other objects, -- making this object display with them nam = 'salt shaker'; -- name that can be -- used as a reference in some functions -- displayed in the inventory panel disp = 'Salt Shaker'; -- displays a highlighted hotlink for anything within -- curly braces, i.e., {salt shaker} dsc = 'There\'s a {salt shaker}.'; -- displays when player clicks the hotlink above tak = 'I took hold of the salt shaker of the table'; -- displays when the player uses the item on another item in her inventory inv = 'It's the corner of the table.'; };
Objects are represented in the inventory panel by the nam
or the disp
attribute, disp
taking precedence. Also, disp
can be set to false
which makes the object invisible in the inventory.
Text enclosed with curly braces { }
inside the dsc
attribute will be displayed as a hotlink in the graphic interpreter.
The act
event handler must be a string, a boolean value, or a function returning one of these two.
WARNING: Some objects (table, io, string, etc…) are reserved in the Lua namespace. Be careful not to use any reserved words when choosing variable identifiers for your objects. In modern INSTEAD versions this problem is almost solved. However, you still will not be able to use variable identifiers that match INSTEAD constructors (obj, game, player, list, room, dlg, etc…)
To include an object to a room or another object add the intended object's variable to the target room/object's obj table. For example salt
can be bound to the tabl
object this way:
Add references to the obj
table to place corresponding objects at the room:
tabl = room { nam = 'Table', dsc = 'It\'s a large kitchen table with a bowl of {salt}.', obj = { 'salt' }, };
You can also use an unquoted variable identifier instead of a quoted reference, but you must define the object before defining the room in order for this to work. References have no such limitation, therefore using references is highly recommended.
Separate multiple objects within a list with commas or semicolons (standard Lua delimiters):
obj = { 'tabl', 'apple' };
Feel free to use line breaks to make your code more readable.
obj = { 'tabl', 'apple', 'knife', };
You can also use functions to place objects in rooms, which will come later.
Any object may have an obj
list. The parent object displays its dsc
then carries out the same operation for each of its children inside obj
. For example let's place a bowl
on the desk
.
main = room { nam = 'A Kitchen', dsc = 'You walk inside.', -- These references can be defined after because -- they're housed within quotes obj = { 'window', 'desk' }, }; window = obj { nam = 'Window', dsc = [[Diffused daylight pours through a round porthole in the middle of the ceiling.]], }; desk = obj { nam = 'Desk', dsc = [[There's a desk in the center of the room.]], obj = { 'bowl' }, }; bowl = obj { nam = 'Bowl', dsc = [[On the surface there's a metal bowl covered in strange engravings.]] };
First the player will see the room dsc
with a large line break, then the window's dsc
, the desk's dsc
, and finally the bowl's dsc
(it's the desk's child object).
Also if you move an object to another room, nested objects will be moved with it. This is part of the referencing mechanism: an obj
table stays bound to its parent object. So be diligent about keeping track of nested objects.
Players often try to use objects in their inventory on other objects. To satisfy this basic urge, the use
handler can be invoked for the held object, and used
for the targeted object. For example:
knife = obj { nam = 'knife', dsc = [[There's a {knife} on the chair]], inv = [[It's sharp!]], tak = 'I took the knife!', use = 'You try to use the knife.', }; chair = obj { nam = 'chair', dsc = 'There is a {chair} in the room.', act = 'Hm... Just a chair...', obj = { 'apple', 'knife' }, --you monster! used = 'You try to hurt the chair...', };
If the player takes the knife and uses it on the chair, he gets the text of knife's use
handler along with the chair's used
handler. These two can also contain functions. The first parameter s
refers to the object itself, and the second parameter w
is the target object if you're defining the use
handler, the affected object if you're defining the used
handler.
knife = obj { nam = 'knife', dsc = 'There is a knife on the {chair}', inv = 'Sharp!', tak = 'I took the knife!', use = function(s, w) if w ~= tabl then p [[I don't want to cut this.]] return false else p 'You incise your initials on the chair.'; end end; };
In the example above you can only use the knife on the chair. If use
returns false then used
won't be invoked, and its return value is ignored. Also, if these two handlers aren't defined or they return nothing the default handler game.use
is called.
It's your choice which one to implement in any given situation, but it's a good idea to place the handler near the object it affects. For example, a trash bin that allows the player to throw anything inside makes sense to employ the trash bin's used
handler.
trash = obj { nam = 'trash'; dsc = [[I see a {trash bin}.]]; act = 'It is not for me.'; used = function(s, w) remove(w, me()) p [[I do not need it anymore.]]; end; }
Bugs often occur when use
and used
are implemented by two unintended objects the player decides to play with. For example, if the player has a knife you intend for her to use on an apple, but she decides to cut the trash bin, the knife will vanish. We'll need to rework the knife's handlers to correct this behavior. The nouse
handler should be implemented:
.......................... require "nouse" .......................... knife = obj { nam = 'knife', use = function(s, w) if w ~= apple then -- if the apple is not an action subject return end if w.knife then return "It's already skinned." end w.knife = true p 'I skinned the apple.' end; nouse = [[I don't want slash it.]]; };
The nouse
handler will be called by the engine if neither use
or used
returns something adequate. If nouse
isn't called, then the noused
handler within the affected object will searched for and called. If none of these result in an adequate return value, then the default handler game.nouse
will be deployed.
All of these usage handlers can be functions, each with three arguments (the handler owner object, the actor object, the target object).
NOTE: The nouse
handler actually redefines game.use
so you must switch to its alternatives such as game.nouse
.
It's also possible to use a scene object (placed in scene, not in inventory) on other objects. If you are going to implement this mechanism, you need to set an object or global game.scene_use
boolean attribute scene_use
, or build a function that returns a boolean value.
stone = obj { nam = 'stone1'; var { pushed = false }; scene_use = true; dsc = [[I see the heavy {stone} on the floor.]]; use = function(s, w) if w == door then s.pushed = true p [[I pushed stone to the door.]] else p [[I do not want to do a hard work!]]; end end; } -- here, this object in scene can be used on other -- objects in scene, like the inventory item...
The player is represented by the object variable pl
of type player
. In the engine it's represented this way:
pl = player { nam = "Incognito", where = 'main', obj = { } };
You can target the current player object with the me()
pointer. In most instances me() == pl
.
The obj
table inside pl
represents the player's inventory. Also, if you want the player to have special qualities you can implement them in the var
table (such as power, speed, etc.):
pl = player { nam = "James"; where = 'main'; var { power = 100 }; obj = { 'apple' }; -- let's give him an apple };
You can even create multiple players (pl2, pl3, pl4, etc.) and alternate between them using the change_pl()
function. It only has one parameter, which targets the player object being switched to. Using this function will change the current game location to whereever the target player is located.
A player's inventory exists inside the player object. The easiest way to put something there is to define its tak
handler.
apple = obj { nam = 'apple', dsc = 'There is an {apple} on the table.', inv = function(s) inv():del(s); --//inv()// is -- a pointer that you can use in the shorthand -- Lua syntax describes before to target the -- inventory of (s) which refers to apple return 'I ate the apple.'; end, tak = 'You took the apple.', -- same as -- the act handler, only it puts the -- target object in the inventory panel };
Examine the inv
handler above. After the object has been take, the player can click it in her inventory panel. The apple is then removed from the inventory and the returned action text is displayed:
'I ate the apple.'
It's also possible to implement taking inside the act
handler:
apple = obj { nam = 'apple'; dsc = 'There is an {apple} on the table.'; inv = function(s) remove(s, me()); -- remove(parent target, consumer) -- me() is the player pointer. The effect of -- consuming an inventory item can be defined p 'I ate the apple.' end; act = function(s) take(s) --take() is another method p 'You took that apple.'; end; };
If an object doesn't have an inv
handler then game.inv
will be called.
You should also be aware that the term “inventory” is loosely defined in the game engine, so the inventory panel could even house abstract objects such as interactive command buttons and status meters. Go wild.
In fact, you may use multiple characters for switching between different types of inventory. Like this:
instead_version "2.1.0" pl1 = player { nam = '1'; where = 'main'; } pl2 = player { nam = '2'; where = 'main'; } inv_sw = menu { nam = function(s) return ('Inv:'.. stead.nameof(me())) end; menu = function(s) if me() == pl1 then pl2.where = here() change_pl(pl2) else pl1.where = here() change_pl(pl1) end end } apple = obj { nam = 'apple'; } knife = obj { nam = 'knife'; } function init() change_pl 'pl1' place(inv_sw, pl1) place(inv_sw, pl2) place(apple, pl1) place(knife, pl2) end main = room { nam = 'room 1'; dsc = 'room 1'; way = { 'r2' } } r2 = room { nam = 'room 2'; dsc = 'room 2'; way = { 'main' } }
The game itself is represented as the object game
of type game
. The engine defines it as follows:
game = game { codepage = "UTF-8", nam = "INSTEAD, version "..stead.version.." '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 = 'p1', showlast = true, _scripts = {}, };
As we can see, the game object holds the current player pl
reference and some settings. At the beginning of your game you can set the text encoding to anything you want, but keep in mind it's better to just switch your editor to UTF-8 without BOM!
encoding and work with the game engine. Changing the encoding is useful when running games written for other platforms (like URQL).
game.codepage = "cp1251";
The game object also contains your default handlers for act
, inv
, use
(It's best to always define them!).
game.act = 'You can\'t.'; game.inv = 'Hmm... Odd thing...'; game.use = 'Won\'t work...';
NOTE: Don't forget that the nouse
handler reserves the game.use
default handler for its own peculiar needs, so if you implement nouse
then you'll need to to include a game.nouse
handler as a fallback for use
to prevent bugs.
Since version 1.1.0
of the SDL version, the game engine has incorporated a timer object.
timer:set(ms) --sets the timer interval in milliseconds timer:stop() --stops the timer timer.callback(s) --creates a callback for the timer, calling it in a fixed time interval
You can create a function that will return a stead interface command invoked after the callback's execution. For example:
timer.callback = function(s) main._time = main._time + 1; return "look"; end; timer:set(100); main = room { _time = 1, forcedsc = true, nam = 'Timer', dsc = function(s) return 'Example: '..tostring(s._time); end; };
Sometimes we need to fill a room with non-functional decorations to make the game world seem more diverse. Lightweight objects may be used for this purpose. For example:
sside = room { nam = 'southern side', dsc = [[I walk to the southern wall of the building. ]], act = function(s, w) if w == "porch" then -- this references -- the lightweight objects created -- below in the obj list ways():add('stolcorridor'); p [[There's an entrance with a porch. The sign on the door reads 'Canteen'. I'm not hungry... Should I go in?]]; elseif w == "people" then p "The people leaving look quite content."; end end; obj = { -- this is a lightweight object vobj("porch", "There is a small {porch} by the eastern corner."), --another lightweight vobj("people", [[From time to time the porch door slams letting {people} in and out..]]) }, };
The method vobj()
allows you to create a lightweight version of a static object. It can be interacted with by means of its parent's act
handler which assesses the child's name. The vobj()
method will call the used
handler. The third parameter can call a separate object to interact with it. The vobj()
syntax is simple:
vobj(name, description, interactive_object)
NOTE: Lightweight objects often don't have handles to help you with interactions via inventory items, etc., so if you want the player to be able to access them you can define those behaviors within their use
handler, via the stead.nameof()
method, like so:
use = function(s, w) if stead.nameof(w) == "humans" then p "You should not disturb humans." return end end;
One useful trick is adding vobj()
to the room dynamically with a place()
method:
place(vobj("cont_button", "{Continue}"));
But this style is old-fashioned. Instead try using a disable()
or enable()
method within your static object descriptions, like so:
obj = { vobj("cont_button", "{Continue}"):disable() }; ....................................................... exist('cont_button'):enable(); -- returns the object -- if it's present regardless of "able" state, then enables it
A useful permutation of vobj()
is vway()
which creates a reference leading to a specified room. Here's the syntax:
vway(name, description, destination_room)
And an example implementation in an obj list:
obj = { vway("next", "Press {here} to pass to the next room.", 'nextroom') }
If you're creating a game in the storybook genre where the gameplay consists of wandering from one link to another (which is probably a poor decision for your first game!) you should utilize the xact()
module. It provides a simpler mechanism for creating references and also has the xwalk
feature.
instead_version "1.9.0" require "xact" main = room { nam = 'Begin'; forcedsc = true; dsc = [[ Begin {xwalk(startgame)|adventure}? ]]; } startgame = room { dsc = [[ A long time ago ...]]; }
Using simple methods like add()
and del()
you can dynamically fill a room with vway()
objects:
objs(home):add(vway("next", "{Next}.", 'next_room')); -- some code here home.obj:del("next"); -- home.obj is legacy style, use objs(home) instead
This behavior is due to vobj()
and vway()
being generic objects with predefined handlers and save methods, which allows on-the-fly creation. Knowing the engine architecture will help you implement object variants like these.
In addition to using lightweight objects for scene decoration, you can also define a static object directly in the obj
list without a handle:
hall = room { nam = 'living room'; dsc = [[I'm in a spacious living room.]]; obj = { obj { nam = 'table'; dsc = [[There's a {table} in the middle.]]; act = [[It looks like mahogany.]]; }; }; }
You can also target such objects via stead.nameof()
within the use
handler, just like you did with vobj()
lightweight objects:
use = function(s, w) if stead.nameof(w) == 'table' then p [[I don't want to spoil such thing of beauty.]] return end end;
(NEEDS AN EXAMPLE)
You can also use a single object in multiple rooms, such as shotgun shell
which gets duplicated as it's dropped into the player's current room each time she fires her shotgun
. In this case the shells might merely serve as decoration without being interactive.
NOTE: It's recommended you study constructors before taking on this supplementary concept.
You can use the new()
and delete()
methods to create and remove objects dynamically. The new()
method can accept a string argument that's written in the form of a constructor, which will of course return an object. Yay!
-- adds a constructor definition to the code new ("obj { nam = 'test', act = 'test' }");
Better yet:
-- places a new constructor in-world place(new [[ function myconstructor() local v = {} v.nam = 'test object', v.act = 'test feedback', return obj(v); end;]]);
The constructed object will be saved every time the game is saved.
Something else to have fun with is the fact that new()
adds a real object to the world, so you can find its variable identifier using the deref()
method then do whatever you want with it, like delete it.
cool_object = deref(new('myconstructor()')); delete(cool_object);
NOTE: This is an advanced topic so there's no need for beginners to study this section in detail.
Dynamically created references can be implemented in various ways. The example below uses vway
objects. Notice, it's most convenient to create dynamic references in the enter
handler, but you're free to leave them wherever they fall in your code.
To add a reference you can use the add()
method.
objs(home):add(vway('Road', 'I noticed a {road} going into the forest...', 'forest'));
To delete a reference you can use the del()
method.
objs(home):del('Road');
The srch()
method will check if the reference is present in the target room.
if not objs(home):srch('Road') then objs(home):add(vway('Road', 'I noticed a {road} going into the forest...', 'forest')); end
If a reference is created in the current room, the previous example can be simplified.
if not seen('Road') then objs():add(vway('Road', 'I noticed a {road} going into the forest...', 'forest')); end
Or you can manage references with enable()
and disable()
.
seen('Road', home):disable(); exist('Road', home):enable();
Creating initially disabled vobj
and vway
references works like this:
obj = {vway('Road', 'I noticed a {road} going into the forest...', 'forest'):disable()},
Enabling them by their index in the obj
table or by finding them with srch
is similar.
objs()[1]:enable(); -- but better variant is exist 'Road':enable();
Most attributes and handlers can be defined as functions. For example the nam
attribute could just be defined as a string like so:
nam = 'apple';
Or you could be fancy and define it as a function that returns that string, like so:
nam = function() return 'apple'; end;
Keep in mind the nam
attribute must be defined as a string, even if you put some extra mojo inside its definition.
Strings passed to any of the p()
methods are accumulated in an internal buffer for that specific attribute/handler. The accumulated content is passed back to the engine when the attribute/handler reaches its return. With this you can build a long text out of sequential calls to p()/pn()/pr()
.
NOTE: The engine performs general formatting with spaces and line breaks to separate corresponding text parts. So, usually you do not need to do manual formatting. Use pxxx-functions to format a single attribute value.
The principal difference between handlers and attributes is that handlers can change the game world's state, while attributes cannot. So, if you define an attribute as a function remember an attribute's only purpose is to return a value. This is because the calling of attributes within the engine as a player roams around freely is often unpredictable and isn't bound to a specific game process, so their application within mechanisms is strongly discouraged.
Something to note about the always mechanism-friendly handler is that it cannot pause the engine to wait for an event to happen. That means you can't employ delay loops or other game delay mechanisms inside the handler itself. They're intended to change the game state and immediately return control to the game engine. The interpreter must deliver the handler's changes quickly so it can continue listening for player input. If you need to delay something use the timer
or cutscene
modules.
Handler functions almost always contain conditional statements when dealing with variables.
apple = obj { nam = 'red apple', dsc = function(s) if not s._seen then p 'There is {something} on the table.'; --first entrance description else p 'There is an {apple} on the table.'; --alternate description end end; act = function(s) if s._seen then p 'The same apple, huh...'; else s._seen = true; p 'Wow! It\'s an apple!'; end end; };
The first parameter in a function (s)
always refers to the parent object it self. When the player enters the room from this example her display will read “There's something on the table.” When the player clicks something the _seen
variable inside the apple
object will be set to true
, which plays in nicely to the dsc
function above it.
The stored variable _seen
has an underscore at the beginning to signify it should be saved in the player's savegame file automatically. Now every time she returns to the room the game knows she's already seen the apple and will display the second description.
When an if
statement is built without an operator ==, ⇐, ~=,…
, the variable is judged as a boolean value. Therefore, being defined as anything other than false
means it's true
. So for example:
if s._seen then --asks "Does s._seen exist and is it not false?" ACTION --if the first statement is true this is performed else --deduces that s._seen doesn't exist or was defined as false ACTION --so this is performed instead end
When a numeric variable hasn't been defined and it's used in a conditional expression like the one above, the engine assumes the variable equals emptiness nil
. You can check if a numeric variable exists with a similar code like this:
if z == nil then p "Global variable z does not exist." end
The nil
variable is also treated as false
in conditional expressions:
if not z then p "Variable z is not defined or false." end
But:
if z == false then p "Variable z is false." -- z == nil will not pass this condition!!! end
NOTE: The z
variable above could be undefined and so, condition z == false
will not work, that might produce a bug in your game logic, so double check!
You can also cascade conditional statements using then
as a delimiter, like so:
if not z then if z ~= nil then p "Variable z equals false." else p "z is undefined! Sell your property and go hunt bugs!" end end
Take these behaviors into account when writing and debugging your game. If you have a misprint in a variable name, the expression will be evaluated as “undefined” with no error report. Your game logic will be broken and it will be difficult to find the error later.
Considering the dynamic room dsc
in the above example is generated with a function, why not alter the dsc
on the fly? Actually this won't work unless you define the dsc
in the room's var
table like this:
button = obj { nam = "button"; dsc = "There is a big red {button} on the room wall."; act = function (s) here().dsc = [[Everything changed in the room!!!]]; pn [[The room transformed after I pressed the button.^ The book-case disappeared along with the table and the chest,^ and a strange looking device took its place.]]; end }; r12 = room { nam = 'room'; var { dsc = [[I am in the room.]]; }; obj = {'button'} };
It's highly recommended to NOT do this. Instead, write your attributes themselves as functions, and refrain from changing their values externally. Such programming style is costly, since you're making future modifications difficult by not keeping attribute values inside the objects they describe, and your savegame files will consume much more disk space.
Standard attribute lists such as way
or obj
store objects in a table and manage themselves with a pre-defined set of methods, but lucky you can create lists for your own needs, and they don't have to be defined through var
or global
. For example:
treasures = list { 'gold', 'silver' };
Your available list methods are:
add(object, [pos])
–add the object to this list, at the optional positioncat(b, [pos])
–insert the content of “b” (another list object) into this list at positionzap()
–clear this entire listdel(object)
–remove the object from this list, but ONLY if it's not disabled! (see next method)purge(object)
–remove any object, EVEN disabled onessrch(object)
–search for the object in this list. If found, return the found item and its indexreplace(old, new)
–replace the old object with the new oneenable(object)
–enable the object, if founddisable(object)
–disable the object, if foundenable_all
–enable all objects in this listdisable_all
–disable all objects in this list
The methods add
, del
, purge
, replace
, srch
and others can receive objects by nam
as well as by a referenced link.
A simple example involves the system method inv()
that references the player's inventory list, which can be quickly manipulated with a list method like so:
inv():del('apple'),
You could even cleverly recreate the take functionality inside the act
handler using list methods:
knife = obj { nam = 'knife', dsc = 'There is a {knife} on the table.', inv = 'It\'s sharp!', act = function(s) objs():del(s); inv():add(s); end, };
The system method objs()
returns the list containing all current room objects, or if given a parameter it can target any room. The corresponding method for getting the list of passages for a given room is ways()
.
Starting in version 0.8
the game
object can be used as a parameter for the add()
method. Also there's an optional second parameter that targets a specific position in the receiving list. You can now also set a list's values by index using the set()
method. For example:
objs():set('knife',1);
Starting in version 0.9.1
the enable_all()
and disable_all()
methods work with embedded objects (objects within object), and the methods zap()
and cat()
became available.
Starting in version 1.8.0
the methods disable()
and enable()
can be used, however…
NOTE: It's highly recommended when manipulating objects and inventory to use higher level methods such as:
place/get/take/drop/remove/seen/have
Sometimes we need to format output from a handler based upon a prescribed set of conditions. In such a situation the p()
and pn()
methods are quite useful. These two methods add formatted text to the internal buffer of the handler, which gets returned by the handler in one big chunk.
dsc = function(s) p "There's a {barrel}." --this gets buffered, with whitespace if s._opened then p "Its lid is leaning against the wall." --this gets tacked onto the first one end end;
One way to update the player with the status of some action is via pget()
in the return.
knife = obj { ... use = function(s, w) -- (w) is another object the player -- is using the knife to interact with if w == apple then -- so if it's an apple -- she says she peeled it p 'I peeled the apple'; -- and the apple stays peeled apple._peeled = true return end p [[You shouldn't use a knife like that.]] return pget(), false; -- false is to stop call 'used' method end;
Sometimes we need a handler doing something without producing any output.
button = obj { nam = "button"; var { on = false; }; dsc = "There is a big red {button} on the wall."; act = function (s) s.on = true return true end; }; r12 = room { nam = 'room'; forcedsc = true; dsc = function (s) if not button.on then p [[This button hasn't been pressed.]]; else p [[The button has been pressed.]]; end end, obj = {'button'} };
Here the act
handler alternates the room description, so there's no need for it to print anything itself. It's a contrived example, and you'll likely never need handlers with no output. As for the above example, you could instead print “Pressed the button.” with the act
handler's function. Moreover, the example requires forcedsc
mode, which breaks backward compatibility. However, such a handler could at some point be useful, so it's worthwhile to be aware of it.
You could also return only true
from an act
handler, which would mean an action completed successfully but didn't require any output. If you need some kind of report for the player, just don't return anything, which will display your default game.act
text. Problem solved.
Sometimes you need to call a handler manually. You can use the shortform Lua syntax for method calls object:method(parameters)
if the handler act
, tak
, inv
, etc., was written as a function within the target object:
-- the function in the act handler of apple is called apple:act()
For more clarity here is the longform of the Lua syntax:
-- so the shortform above simply erases the (self) parameter -- since it's redundant, and replaces the period with a colon apple.act(apple)
If the object's handler is not a function you can utilize the stead.call()
routine to call the handler in the same manner as the interpreter does.
The syntax is: stead.call(object, 'name of handler/attribute', arguments…)
So, for example, to get nam
value of the object apple
you can do this:
act = function(s) -- somewhere local r = stead.call(apple, 'nam') p(r) end
In fact, there is the stead.nameof()
for this case, but this is just an example.
Handlers can be programmed to execute as certain player conditions are met. This technique is often used to run background processes such as life simulation. For this purpose a special event handler exists, called the life
handler.
Any object, including rooms, can employ a life
handler, which is called every time the game clock advances as long as the object has been added to the worldwide list of living objects (more on this in a moment). Remember, the game clock is the total number of player actions so far in the game.
Dynamic events follow this rough algorithm for each tick of the game clock:
life
handlers for all living objects are triggeredTo illustrate a simple living object, first let's give Barsik the cat some animation.
mycat = obj { nam = 'Barsik', behaviors = { [1] = 'Barsik peers out of my coat.', [2] = 'Barsik kneads my chest. OWWW!', [3] = 'Barsik purrs against me.', [4] = 'Barsik yawns.', [5] = 'Barsik\'s warmth is welcome.', [6] = 'Barsik escapes from my coat.', }, life = function (s) local r = rnd(5); if r > 2 then return; --provides a 60% chance of no behavior end r = rnd(#s.behaviors); --the # symbol gets the last index number p(s.behaviors[r]); --displays a random behavior end; };
Now that Barsik looks like a living creature, let's make it final.
lifeon('mycat');
Okay, like you were told before, this is the part about the worldwide list of living objects. The lifeon()
method as seen in the code above, adds Barsik the cat to this list. Don't forget to remove deceased objects from the list with lifeoff()
, usually via the exit
handler.
If there are a dizzying number of living objects in your game, you may want to assign priority levels. The second parameter of lifeon()
takes a positive integer, with 1
being highest priority.
lifeon(object_name, positive_integer);
To initialize a life simulation background process for a particular room, simply include lifeon()
in an object's entered
handler and lifeoff()
in the left
handler, then define the process in the life
handler.
cellar= room { nam = 'in the basement'; dsc = [[It's dark down here.]]; entered = function(s) lifeon(s); end; left = function(s) lifeoff(s); end; life = function(s) if rnd(10) > 8 then p [[I heard something rustling!]]; --scare tactics! end end; way = { 'upstair' }; };
You can discern whether the player has crossed a room's border by employing the player_moved()
method inside the life
handler.
flash = obj { nam = 'flashlight'; var { on = false }; life = function(s) if player_moved() then -- extinguishes the flashlight on room changes s.on = false -- let's the player know what happened p "My light went out..." return end end; ... };
To track continuous events you can use the time()
method, or utilize a custom counter variable. To track player location use here()
function. Use these in conjunction with the live()
method, which checks if an object is still living.
dynamite = obj { nam = 'dynamite'; dsc = 'BOOM!'; var { timer = 0 }; used = function(s, w) -- checks if the player uses their zippo lighter if w == zippo then if live(s) then --checks redundant return "It's already lit." else -- the player lights it p "I ignited the fuse." -- the dynamite is added to list of living objects lifeon(s) end end end; life = function(s) -- if it's living/lit the timer starts s.timer = s.timer + 1 if s.timer == 5 then -- when it reaches 5 the dynamite dies lifeoff(s) if here() == where(s) then -- checks if player is in the same room p [[...yeah I'm dead.]] else p [[I heard an explosion somewhere.]]; end end end; };
If a life
handler returns a string, it displays below the room's static description by default. To make the text appear above, simply return true
as it's second value.
life = function(s) return "The guardian entered the room.", true; end;
If you want to prevent all life processing in a particular room, add the nolife
module to your game.
instead_version "1.8.2" --needs to be this version or higher require "hideinv" require "nolife" ... guarddlg = dlg { nam = 'Guardian'; hideinv = true; nolife = true; ... };
WARNING: When putting the walk()
method inside a room's life
handler you should take into account whether the life
handler will affect player location. If so, any results from the handler will will be discarded at the point of player movement, then her new location's life
handler will activate. This can get confusing, so it's best to implement walk()
carefully.
The life
handler is capable of removing all act
handler text from the display, in order to let more pressing events dominate the player's focus. Imagine the player is examining a regular window in a boring room…
window = obj { rainrain = false; ... act = function(s) s.rainrain = true; --the variable that triggers the goblin return "It's probably about to rain. I wonder where my umbrella is..." end; };
Suddenly an evil goblin enters the room and runs right for her. In such a panicky situation, the window's act
handler telling her about rain and umbrellas is useless information. The following code removes it and allows the goblin's life
handler to take center stage.
goblin = obj { ... life = function(s) if rainrain == true then p("A goblin's running toward me!"); -- removes all act handlers' text from the display ACTION_TEXT = nil; end; end };
The ACTION_TEXT
variable is unique to life
handlers because of their clock-based routine, and allows them to change the output of all act
handlers in the room at once. It's usually best to simply clear it with nil
. This way the player can focus on the goblin. :O
The game engine provides several standardized methods for returning frequently used objects (Methods are also called functions, but this manual tries to reserve the term “function” for your own code definitions, and tries to use the term “method” for anything pre-defined within the engine, simply to make it easier for young developers to discern what's being addressed in this manual).
The following terminology is used throughout this section, so refer to it as you study the methods themselves.
Common Terminology
[ ]
– encloses optional parameters Methods Returning Lists
inv()
–returns the inventory objects listobjs([where])
–returns the objects list of the current room or of an object passed as the optional parameterways([room])
–returns the passages list for the current room or for one passed as the optional parameterMethods Returning Objects
me()
–returns the current player objecthere()
–returns the current roomwhere(object)
–returns a room or an object that was placed using a put/move/drop/replace
method, or othersfrom([room])
–returns the previous current room of the player or the room visited prior to the room passed as the optional parameterseen(what[, where])
–returns the object in the current or optionally targeted room, if the object is present there and is not diabled (see exist()
)exist(what[, where])
–similar to seen() but will find disabled objectshave(what)
–returns the object (what) if it's present in the inventory and is not disabledlive(what)
–returns the object (what) if it belongs to an alive object (read further in wiki)path(passage[, room])
–returns the passage from the way list of the current room or one passed as the optional parameter, even if it is disabledThe methods used in the following example are usually used in conditional statements or to find objects for subsequent modification.
exit = function(s) -- remember that Lua allows solo parameters to go without parentheses if seen 'monster' then p 'The monster blocks your way!' --another solo parameter return false end end; ... use = function(s, w) if w == window and path 'Through the window':disabled() then -- checks whether actions on the window and -- its corresponding passage are currently -- disabled in the game --enables them path 'Through the window':enable(); -- dramatizes with descriptive fiction. Yay! p 'I broke the window!' end end; ... act = function(s) if have('knife') then p 'But I have a knife!'; return end end;
Two useful system methods are:
stead.ref(reference)
stead.deref(object)
The first returns a link to the object passed by reference, the second one returns a reference to the object passed directly. This might seem confusing now, but you'll soon understand the concept of references versus objects. Here's an illustration:
-- if apple is defined, the reference is equal to the variable itself stead.ref 'apple' == apple
Here's a simple example of stead.deref()
in action:
act = function(s) -- this takes the object, s, that was passed to the -- function directly and returns a reference so that -- the print method can concatenate it with its string p('You clicked the object '.. stead.deref(s)); end;
A code statement is useful for defining a very short function. To save them you can assign them to a stored variable in the global
or var
tables. Here's an example of a short code
function called in an act
event handler:
act = code [[ walk(sea) ]];
You can also redefine code
functions on the fly, although this is usually an example of bad programming.
var {walkwater = code [[ walk(sea) ]]; }; ... s.walkwater = code [[ walk(ground) ]];
Invoked code
statements can create objects automatically. They are also “self” variables that target the object that contains them. An args[]
table holds all the code
statement's arguments.
dsc = code [[ if not self._seen then p '{Something} lays on the tabe.'; else p 'There is an {apple} on the table.'; end ]];
NOTE: The code
statement above is already surrounded with double brackets, so if you need to write a multi-line string literal inside a code
statement you must use nested brackets such as [=[ ]=]
, [==[ ]==]
, [===[ ]===]
, etc. The same is needed for delineating line breaks with the ^
symbol. Study the Lua syntax rules for more detail.
Sometimes you may need to create an auxiliary variable in the var
table to store a temporary value, for example:
kitten = obj { nam = 'kitten'; var { state = 1 }; -- state variable --(s) refers to the entire kitten object, and -- this act handler function is triggered by each click act = function(s) -- increments the "state" temporary value -- by 1 with each click s.state = s.state + 1 -- iterates until it reaches 3 if s.state > 3 then -- resets it to 1, creating a clickable looping effect s.state = 1 end p [[Purrr!]] end; -- this dsc attribute function's table is related -- to the handler function above through the index dsc = function(s) local effects = { "The {kitten} is purring.", "The {kitten} is playing.", "The {kitten} is washing up.", }; -- Now the index corresponding to the -- click loop in "act" can be printed to the player's screen -- s.state is a numerical index for the -- effects table which has 1,2,3 (a Lua index begins at 1) p(effects[s.state]) end; }
In the dsc
function above, a local table is created. The keyword local
constrains the table's visibility to within the function itself, meaning nothing outside can see or use it. And local
variables are not written in save files. You should define all auxiliary/temporary variables as local
.
The example could also be written like this:
dsc = function(s) if s.state == 1 then p "The {kitten} is purring." elseif s.state == 2 then p "The {kitten} is playing." else p "The {kitten} is washing up.", end end;
Or like this with an external function:
function mprint(n, ...) -- temporary table for the function arguments local a = {...}; -- prints the nth element of the temp table p(a[n]) end;
Which can be called inside the dsc
attribute:
dsc = function(s) mprint(s.state, { "The {kitten} is purring.", "The {kitten} is playing.", "The {kitten} is washing up.", }); end;
To simplify it all you could write it like this:
dsc = function(s) p(({ "The {kitten} is purring.", "The {kitten} is playing.", "The {kitten} is washing up.", })[s.state]); end;
Savegame files store the delta (change) between the initial and current game state. They can store these types of variables:
There are three ways to create a stored variable.
Using var
and global
is more intuitive so it's recommended.
Defining the global table is simple:
global { -- a number stored in the savegame file gl_var = 1; -- another number gl_num = 1.2; -- a string gl_str = 'hello world'; -- a boolean knows_truth = false; }
Defining a var
table inside an object is also simple:
main = room { var { -- this will be stored in the savegame file i = "a"; z = "b"; }; nam = 'My first room.'; var { -- also stored new_var = 3; }; dsc = function(s) --(s) refers to main, so s.i refers to -- the variable "i" that's housed in -- main's var table p ("i == ", s.i); -- global elements are mapped to the -- global namespace. It's as if "global" -- and "var" tables are excluded from the -- object tree p ("gl_var == ", gl_var); end;
Every element inside the var
and global
tables must have an initial value. A system dummy object called null
will serve as a default placeholder if you don't define a value. It can be replaced by your game code later.
The game engine has a number of high-level methods that may be useful to you. You've already encountered some of them.
-- moves the object from the current -- or given room to a given destination move(what, where_to[, where_from])
If you want to move an object from an arbitrary room you must to know its location. For implementing objects moving in a complicated fashion you could write an advanced function to track the object's location as it moves between distant rooms, or you could locate the object with the where()
method each time, like so:
move(mycat, here(), where(mycat)) --mycat travels with me
NOTE: You must handle the moving object with a place()
method in order for the where()
parameter to work.
The movef()
method is similar to move()
but the target object is sent to the beginning of the destination obj
list.
The twin methods drop()/dropf()
and take()/takef()
act in the same way except they focus on the player's inventory.
drop(what[, where])
–removes the object from the player's inventory and places it at the end of the current or optionally given room's obj
listdropf(what[, where])
–drops it at the beginning of the target room's obj
listtake(what[, where_from])
–removes the object from the current or optionally given room and places it at the end end of the player's inventorytakef(what[, where_from])
–adds the object to the beginning of the player's inventory
The four methods above can materialize objects in the target obj
lists even if the object wasn't present in the source location. This trick is often used for filling the player inventory in the init
function.
function init() -- this knife doesn't exist anywhere, -- it simply materializes in the player's -- inventory list. Yay! take('knife'); end;
The following methods all work in a similar fashion to those above.
place(what[, where])
–places the object at the ending of the current (or optionally given) roomplacef(what[, where])
–places the object at the beginningreplace(what, what_with[, where])
–replaces the first object with the second in the current or optionally given roomremove(what[, where])
–removes an enabled object from its current location or an optionally given roompurge(what[, where_from])
–same as remove()
but disabled objects may be removed as wellThe methods above can be applied directly to rooms and objects as well.
-- uses the me() pointer to access the -- 'pl' object's inventory list remove(apple, me())
Some methods above have variations suffixed with to which receive an additional parameter, the specific position for the object to be inserted into the target list.
dropto(what[, where], pos)
–since version 2.2.0taketo(what[, where], pos)
placeto(what[, where], pos)
More methods include:
lifeon(object[, priority])
–adds the object to the dynamic “alive” objects list (explained more later). The optional parameter must be a numeric value, with 1
representing the highest prioritylifeoff(object)
–removes the object from the alive listtaken(object)
–returns true if the object has been taken (with tak
handler or a take()
method) and defaults to falsernd(m)
–returns a random integer in the range from 1
to m
You will often move the player to different rooms. It's important to mention that calling walk()
triggers a corresponding exit/enter/left/entered
handler, which may occasionally cancel the player's movement. Continue reading to learn how to manage this situation when it arises.
Here's the basic format for walk()
followed by an example:
walk(where_to)
–moves the player to the specified room....................................... act = code [[ pn "I'm going to the next room..." walk(nextroom); ]]
Here's an example of passing a function reference to the walk()
method:
mycar = obj { nam = 'my car', dsc = 'In front of the cabin there is my old Toyota {pickup}.', act = function(s) walk('inmycar'); end; };
NOTE: walk()
doesn't interrupt the parent handler's execution, so you should typically place your function's return operator immediately after walk()
like this:
act = code [[ pn "I'm going to the next room..." walk(nextroom); return ]]
To change players:
change_pl(player) -- switches the game to the specified player, -- who has an independent inventory and location
When changing players the exit/enter/left/entered
handlers are not triggered.
If you'd like to simulate offscreen movement for an inactive player without having to change players, you can simply change her location using the where
attribute:
jane.where = 'Living Room'
Here are even more auxiliary functions:
walkback([where_to])
–moves the current player to the previous room, or to a specified room, and the player's from
property will not be changedback([where_to])
–similar to walkback()
except when using back()
to move from a dialogue to a room, the room's dsc/enter/entered
handlers will not be called, but the dialogue's exit/left
handlers will be calledwalkin(where_to)
–moves the player to the specified room, while the current room's exit/left
handlers are not calledwalkout()
–moves the player to the previous room, and its enter/entered
handlers are not calledtime()
–returns current game time, calculated by the number of player actions performed so farcat(arg1,arg2,arg3,etc…)
–returns a concatenation of its arguments, but returns nil
if the first argument is nil
par(arg1,arg2,arg3,etc…)
–returns a concatenation of its arguments, each delimited by the content of the first argumentvisited([room])
–returns the number of times the current or specified room has been visited, or nil
if nevervisits([room])
–similar to visited()
but returns zero instead of nil
player_moved([player])
–returns true if the player has moved this game tick, usually used for life
handlers
In cases where forcedsc
is incompatible with your intentions you can use the following method to maintain compatibility:
stead.need_scene([room])
–makes the engine show the room's static dsc
during the next game tickstead.nameof(object)
–returns the object's nam
attributestead.dispof(object)
–returns the object's disp
attribute, or nam
if disp
is emptydisabled(object)
–returns true if the object is disabledstead.call(link, attribute_handler_string_name, parameters)
–calls the handler, or returns the attribute value (this method is explained further later)instead_gamepath()
–returns the full path of the game directoryinstead_savepath()
–returns the full path of the saved game files directoryYou can accomplish simple text formatting with the following methods:
txtmiddle()
–aligns text at the middle of the screen on both axes (this is the default setting)txtc()
–longitudinally aligns text in a central column on the screentxtr()
–longitudinally aligns text to the right side of the screentxtl()
–longitudinally aligns text to the left side of the screentxttop()
–latitudinally pushes text to the top of the screentxtbottom()
–latitudinally pushes text to the bottom of the screenFor example:
main = room { nam = 'Intro', dsc = txtc('Welcome!'), };
You can define a special text style with these methods:
txtb()
–boldfacetxtem()
–embossedtxtu()
–underlinedtxtst()
–strikethroughFor example:
main = room { nam = 'Intro', -- only boldfaces the word "main" dsc = 'You are in the room: '..txtb('main')..'.', };
Since version 1.1.0
you can create unwrapped strings using the txtnb()
method.
main = room { nam = 'Intro', dsc = 'You are in the room '..txtnb('main')..'.', };
WARNING: If you're writing your first game you should make it simple, that is to say the information in this section of the manual is not important. Moreover, 90% of the games written for INSTEAD don't use anything in this section! That's not to say it's not a good section.
The idea of constructors should come to mind once your (second/third/tenth) game has a lot of similar objects. You might say to yourself, “I really would love to simplify this code…” Constructors are a method for automating common pieces of code. In fact the obj
, room
, and dlg
handlers are constructors themselves.
When you write something as an obj
a constructor method calls obj
as a parameter which passes the table {nam = 'name'}
.
apple = obj { nam = 'apple'; dsc = "It's a nice apple."; };
Say you need a window in your game. Realizing that real windows can be broken, you can write a constructor that will manifest all windows with this realistic behavior, saving yourself from ever having to code it again.
-- first create a variable as a function window = function (v) -- pass itself in as true v.window = true -- assess whether nam is empty if v.nam == nil then -- if so, fill it with this generic title v.nam = 'window' end -- assess whether dsc is empty if v.dsc == nil then -- if so, fill it with this phrase v.dsc = 'There is a {window}' end -- function to define the default act handler -- regardless of whether it's empty v.act = function (s) -- assess whether the act handler's _broken variable is set to true if s._broken then -- if so display this phrase when the player clicks the window p [[broken window.]] else p [[dark outside.]] --otherwise display this end end; -- assess whether the used handler is empty if v.used == nil then -- function to autofill the used handler v.used = function (s, w) -- assess whether the second parameter (parent self?) if w == hammer then if s._broken then p [[Window already broken.]] else p [[I broke the window.]] s._broken = true; end end end; end return obj (v) end;
This constructor fills in some of its attributes and handlers itself, but still allows the player's game and your own extraneous code to override some of them. At the end it calls for the manifestation of the object with finalized parameters and returns the custom window object.
win1 = window { -- overrides only the dsc attribute, while all others auto-fill dsc = "In the eastern wall is {window frame}."; };
Since windows are usually static objects, you can could put it directly into the obj
handler for your room instead.
obj = { window { dsc = 'In the east wall is a {window frame}.'; } };
If you want to use a more conventional constructor syntax, as a function that takes several parameters instead of a table it's perfectly workable. In fact, vroom()
, vobj()
, and vway()
follow this paradigm.
window = function (nam, dsc) -- creates an empty table inside function local v = {} v.window = true -- begins to fill the table v.nam = 'window' -- checks if the given window being passed in already has a dsc if dsc == nil then v.dsc = 'There is a {window}.' end v.act = function (s) if s._broken then p [[broken window.]] else p [[dark outside.]] end end v.used = function (s, w) if w == hammer then if s._broken then p [[Window already broken.]] else p [[I broke the window.]] s._broken = true; end end end -- sends the local window settings out -- into the world as a constructed (pre-filled) object return obj(v) end;
A call to this constructor will look different than the table-based calls.
obj = {window ('Window', 'In the east wall is {box}.')}
In fact, both approaches are useful in different scenarios. If the constructed object might be assigned new or optional attributes later, it's easier to define the constructor as a function of the host table, the same as obj
, room
, and dlg
are. Otherwise, if the object is sure to be hands-off later, you can use define it as a function itself.
You might need to add new variables to your constructor. In the example above we created the stored variable _broken
prefixed with an underscore because stored variables can be created on-the-fly. But if you need unstored variables you can use stead.add_var()
instead. The syntax is shown below.
stead.add_var(constructor, {variable = value})
Here's the previous example with an unstored variable.
window = function (v) -- adds the "broken" with a value to your constructor stead.add_var (v, {broken = false}) v.window = true ... end;
If you need to add a global
variable it can be done on-the-fly just like stored variables.
-- no need for parentheses since it's only one parameter stead.add_var {global_var = 1}
Now that we understand how to build constructors, let's think about the concept of inheritance. It's employed in the examples above, at least it will be since there's no other reason to have constructors. They call another constructor window obj
, thereby obtaining all the properties of a regular object. Also, window defines a variable attribute window, to the game, we can understand that we are dealing with a window. For example:
use = function (s, w) if w.window then p [[I point my flashlight at the window.]] return end end;
To illustrate the mechanism, create a class of objects called treasure
.
global {score = 0} treasure = function() local v = {} v.nam = 'treasure' v.treasure = true v._points = 100 v.dsc = function (s) p ('There are {', stead.dispof (s), '}.') end; v.inv = function (s) p ('same', stead.dispof (s), '.'); end; v.tak = function (s) -- Increase the account score = score + s.points; p [[With trembling hands I took the treasure.]]; end return obj (v) end;
Now, based on our treasure
constructor we can define any specific kind of treasure for our world. How about gold, diamond and a treasure chest?
gold = function(dsc) local v = treasure (); v.nam = 'gold'; v.gold = true; v.points = 50; v.dsc = dsc; return v end; diamond = function(dsc) local v = treasure (); v.nam = 'diamond'; v.diamond = true; v.points = 200; v.dsc = dsc; return v end; chest = function(dsc) local v = treasure (); v.nam = 'chest'; v.chest = true v.points = 1000; v.dsc = dsc; return v end;
To put them in the game, just define a variable with them, and as you should be able to tell the inherited aspects from treasure()
fall into diamond()
, gold()
, and chest()
. Cool huh?
diamond1 = diamond("I see a glittering {sparkle in the mud}..."); diamond2 = diamond(); --fills in with the default dsc gold1 = gold("In the corner, a bit of yellow {shimmers}."); cave = room { nam = 'cave'; obj = { 'diamond1', 'gold1', chest ("A heavy steel {chest}!") }; }
When writing constructors sometimes you'll want to run a handler or a method from within. Simply use this system method to access it:
stead.call(object, method/handler, parameters)
You could modify the window constructor you created earlier (example to show syntax, rather than a purposeful modification).
window = function (nam, dsc, customHandler) local v = {} --creates an empty table v.window = true --fills the table v.nam = 'window' v.newHandler = function(s) -- custom event handler that returns a string p("It looks to be antique glass.") end; if dsc == nil then v.dsc = [[There's a blurry plate glass {window}]] end v.act = function(s) if s._broken then p [[It's broken.]] end -- calls the custom 'newHandler' and places its result in r local r = stead.call(s, 'newHandler') if r then p(r) else p [[It's so dark outside.]] end end; ... end;
WARNING: The following section is merely to prevent frustrating misunderstandings with the nature of constructors. This is the fact that it's impossible to save objects created on-the-fly without giving them a variable identifier. To do so requires much deeper engine manipulation than should be necessary for the scope of this engine.
For example, you could try to create a custom version of vway()
, calling it xway()
. When clicked, an xway()
object executes xwalk()
, like so:
function xway(name, dsc, w) -- you can't pass the variable handle itself return obj { nam = name; dsc = dsc; act = function (s) walk (s.where) end; where = w; }; end
When you attempt to implement this in real code it will be impossible to save any object it constructs because there's no variable identifier.
-- creates an handle-less object place(xway('the cave', '{In the cave}', 'cave'));
The engine then won't be able to save the object. This is because generally speaking, INSTEAD is only able to save objects that have identifiers. Objects that only have descriptors are created by explicitly assigning them in a global context such as (apple = obj {…})
or by creating them within lists such as obj
and way
where they're indexed and stored. (Also you can use the inadvisable new()
system method described below).
Nevertheless, there is a way to store these objects, but it isn't useful for writing games. It's intended for the development of modules and extensions of the engine. If you're an average storytelling game designer, you can safely skip the following explanation. In fact, it's the wish of the creator of INSTEAD that designers never need to deal with the engine on this level.
So, to save a variable in a very unfriendly and non-game-code kind of way you can do this:
function xway (name, dsc, w) return obj { nam = name; dsc = dsc; act = function(s) walk (s.where) end; where = w; save = function(self, name, h, need) -- self == the current object -- name == the intended handle of the variable -- h == file descriptor for savegame -- need == a sign that it is creating an object, -- the save file is to create an object at startup local dsc = self.dsc; --startup local w = stead.deref(self.where); if need then -- in the case of creation, -- we write the string to call the constructor h: write (stead.string.format( "% s = xway (%s,%s,%s); \ n", name, -- Formation of lines stead.tostring (self.nam), stead.tostring (dsc), stead.tostring (w))); end stead.savemembers (h, self, name, false); -- keep all other variables such as object, -- the state of the on / off, etc. false -- in last position. -- this will mean they'll be transferred to -- the save-nested objects as the 'need' parameter end }; end;
Character dialogues are actually room objects of type dlg
which contain a phr
object list. There are two types of dialogues: extended and simple. Both share the same behavior, but simple dialogues are deprecated and therefore not recommended for use.
When the player enters a dialogue she will see a list of phrases. When she clicks a phrase it triggers a reaction. By default, clicked phrases become hidden. When all of them have disappeared the dialogue returns the player to the previous room. Often you may want to include permanent phrases like 'End this dialogue.' or 'Ask one more time.' to prevent the dialogue from escaping to the previous room.
Dialogues are entered in the same way as rooms:
cook = obj { nam = 'cook'; dsc = 'I see a {cook}.'; act = function() return walkin 'cookdlg' end; };
It's recommended to use walkin()
instead of walk()
because the current room's exit/left
handlers won't be activated. Also, the person we want to talk to is usually in the same room with the player, so we don't want to officially leave.
You can enter a dialogue from another one, implementing hierarchical branches. You can return to the previous branch with a back()
call. Extended dialogues implement hierarchical branches much better than simple dialogues.
The engine prevents entering a dialogue with no phrases because it will be exited automatically. Take this into account while debugging your games.
NOTE: It's highly recommended to use the hideinv
handler when implementing dialogue into your game. Dialogues will look better if inventory items aren't affecting phrases, and you will prevent unexpected conversation bugs. Add it by putting this at the top of the main.lua
file:
require "hideinv"
Then declare it as true inside your dialogues like this:
guarddlg = dlg { nam = 'guard'; hideinv = true; phr = { {...}; {...}; }; }
The most common mistake is incorrectly calling a dialogue from the inv
handler without hiding the inventory itself from the dialogue. For example, when building a mobile phone inside your game, you would expect the player to click the phone inside her inventory which would trigger a dialogue. As you know from above, escaping dialogues is usually done through a back()
call, but if the inventory isn't hidden and the player clicks the mobile phone in the inventory one more time, we run into the same dialogue again! The back()
call will put the player in the previous instance of the dlg
“room” instead of the actual room she's supposed to be standing in. Of course you can avoid such situations by doing the following:
phone = obj { nam = 'mobile'; -- when the player clicks on -- her mobile phone in her inventory inv = function(s) if here() ~= phone_dlg then -- this line checks if the current room -- is the actual room she's in, not the dlg "room" -- if she's not in the dlg, it makes her walk into the dlg walkin(phone_dlg) return end -- this relates the situation to the player :) p "I am already holding the mobile." end; };
Since version 1.7.0
a more powerful dialogue syntax has been supported, known as extended dialogues. Unlike the earlier format, now phrases are defined in a phrase table:
cookdlg = dlg { nam = 'in the kitchen'; hideinv = true; entered = [[I notice the round face of the cook under her white hat. Her eyelids are sagging...]]; phr = { { always = true, "Some of those greens, please. And beans too.", "Mmmmkay."}; { always = true, "Fried potatoes please.", "Of course."}; { always = true, "Two bowls of garlic soup.", "My favorite..." }; { always = true, "Something light, please. I've got an ulcer.", "No soup for you!" }; { always = true, "Thank you, I don't want anything.", "Get out of the line.", [[back()]] }; }; };
If the dialogue's dsc
attribute isn't defined, it will automatically call the last reply from the player's interactions. The player will see it after clicking the room's caption. Because of this pleasing behavior it's better to put any necessary static description of the dlg
into its entered
handler, as was done in the above example.
Phrases are written with the following syntax:
phr = { [index or tag=TAG], [false if disabled], [always = true], "Question", "Reply", [[reaction code]] };
Each phrase is presented to the player as its “Question” value. The “Reply” value is displayed after it's been clicked. The phrase then becomes disabled (changing its second value to false
), and the reaction code is invoked (if present). The reaction code may contain any Lua code.
The game engine provides the following methods to use on phrases:
pon(indexes or tags)
– unhides phrasespoff(indexes or tags)
– hides phrasesprem(indexes or tags)
– removes/blocks phrases, and they CANNOT be re-enabled with pon()
pseen(indexes or tags)
– returns true if all targeted phrases are visiblepunseen(indexes or tags)
– returns true if all targeted phrases are hiddenIf called with no arguments, these methods are applied to the current phrase, wherever the code is called from.
To manipulate phrases in a different dialogue, you can use the following syntax:
dialogue:method()
For example, if the guard_dlg
exists somewhere outside the current room, you can target it like this:
guard_dlg:pon('show_card')
You will probably find it quite useful at times to disable a phrase initially, then enable it later, like so:
cookdlg = dlg { nam = 'in the kitchen'; hideinv = true; entered = [[I see a fat face of a lady-cook wearing a white hat. She looks tired...]]; phr = { -- this phrase is initially disabled { 1, false, always = true, [[I love those!]], [[Everyone does.]] }; -- once the player finds out there are -- french rolls on the shelf the first phrase is enabled { [[What's on the shelf?]], [[French rolls.]], [[pon(1)]] }; { always = true, "Some greens, please.", "Good."}; { always = true, "Fried potatoes, please!", "Eat now!"}; { always = true, "Thanks, but I'm not hungry.", "As you wish.", [[back()]] }; }; };
If you'll never need to manipulate a certain phrase, you can drop its first field entirely:
{ "Question?", "Reply!" };
To manipulate phrases you can target them by index:
{ 2, "Question?", "Reply!" };
However for complex dialogues, tags are more suitable:
{ tag='exit', "I'm leaving!", [[back()]] };
A tag is simply a string label. As explained earlier, you can pass phrases by index or tag to the pon/poff/pseen/punseen
methods. You can also assign a tag to an indexed phrase.
NOTE: If multiple phrases have the same tag, the method's action will be applied to all of them.
Also be aware that:
pseen()
will return true if at least one of its targeted phrases that has the plural tag is visiblepunseen()
will return true if at least one of its targeted phrases that has the plural tag is hidden
If a phrase contains always=true
it won't be automatically hidden after being clicked by the player.
{ tag='exit', always=true, "Ok, I'm leaving!", [[back()]] }
If you want to implement reaction code without returning any text after the player clicks the phrase, you can use one of the following examples:
-- put "nil" in the Reply field { tag='exit', always=true, "Ok, I'm leaving!", nil, [[back()]] } -- which will return nil simply omit the Reply field, { tag='exit', always=true, "Ok, I'm leaving!", code = [[back()]] }
You can also define questions and replies as functions or code:
-- this phrase unhides the next { tag='exit', code[[p"I'm leaving."]], code[[p"Please stay."; pon'really']] }, -- this phrase is hidden by default { tag='really', false, always=true, "No, I'm really leaving!", function() back() end }
For complex branching dialogues you can group phrases, which essentially removes the need for massively convoluted pon/poff
crossovers between numerous dlg
objects. Phrase groups are displayed to the player as a new dialogue screen.
Each phrase group must be separated by a phrase with no reaction code. The simplest implementation is using curly braces { }
but you can use anything to separate your groups, even a tag which allows you to target switching between branches, like so:
--First Phrase Group { 'Tell me about the weather.', 'Does I look like I keep up with the weather?', [[psub 'weather']] }, { always=true, [[Goodbye.]], code = [[back()]] }, -- this is the delimiter, essentially serving as -- a targetable nametag leading into the following branch { tag = 'weather' }, -- Second Phrase Group { 'Do you know the current temperature?', 'Maybe... 25 Celsius.' }, { 'What the humidity like?', 'Definitely eighty percent. You can bet on it.' },
Only one group can be visible to the player at a time. Upon entering the sample dialogue above, the player sees two phrases to choose from. The first phrase's code pushes her down to the phrase with the tag weather
, which is empty, so it basically falls out of the way and the second phrase group takes over. After both questions in the second group are asked, the player is pushed back to the initial branch, where the only visible phrase left is 'Goodbye.'
Branch switching is implemented with the following methods:
psub('branch')
–pushes the player into the target branch, or the following sub-group by default, and adds a return behavior, such that after all branch phrases are disabled or pret()
is invoked (this method is explained later), the player will automagically return to the caller branch. Nifty!pjump('branch')
–makes an unconditional jump to the target branch, or the following sub-group by default, and adds a return behavior like psub() doespstart('branch')
–makes an unconditional jump, and erases the psub()
call stack, preventing the return behaviorTo get the current branch's index use this method:
dialogue_nam:current()
To get its tag use this:
dialogue_nam:curtag()
You can apply the two methods above to external dialogues as well, like so:
shop_dlg:pstart(1)
A branch's active/inactive status as well as the number of visible phrases remaining. Simply use the following methods, both of which accept the index/tag of the leading phrase in the target branch as their argument. If no argument is supplied, the current group is processed:
-- returns true if the group contains no active phrases dialogue_nam:empty([t]) -- returns the number of visible phrases, 0 if empty dialogue_nam:visible([t])
You can get fancy with how branches are displayed to the player by including a Question field in the target, which becomes a static caption for the entire group:
{ 'Tell me about the weather.', code = [[psub'weather']] }, { always=true, [[Goodbye.]], code = [[back()]] }, { }, -- plays the role of branch caption { tag = 'weather', 'Okay, what are you interested in?'}, { 'What is the temperature?', '25 degrees Celsius.' }, { 'What about the humidity?', '80%.' },
This caption question can also be a function, allowing you to invoke code as branches switch.
{ 'Tell me about the weather.', code = [[psub'weather']] }, { always=true, [[Goodbye.]], code = [[back()]] }, { }, { tag = 'weather', function() p 'Ok, what exactly?'; weather_asked = true; end }, { 'What\'s the temperature?', '25 degrees Celsius...' }, { 'Do you know the humidity?', '80%!' },
The caption phrase can additionally contain an empty method, which will be called after all phrases in the branch are hidden:
{ 'Tell me about the weather.', code = [[psub'weather']] }, { always=true, [[Bye!]], code = [[back()]] }, { }, { tag = 'weather', 'What do you?', empty = code [[p'Enough talking about the weather!'; pret()]] }, { 'Temperature?', '25 C!' }, { 'Humidity?', '80%' },
Despite being included in the above empty method, the default action for empty is pret()
. However, if you choose to redefine empty with some other method you must call pret()
explicitly when needed from then on.
If you want to see a full branching dialogue.. in Russian, check out the following link to the GitHub repository: Example Dialogue
WARNING: The following examples utilize deprecated syntax, and are included in this documentation for the sake of potentially helping you sort your way through obsolete code in old games. Some concepts are shared with Extended dialogues, but it's still important to follow the Extended rules for modern development.
Here's a dialogue written in the deprecated syntax:
food = obj { nam = 'meal', inv = function (s) remove('food', inv()); p "I'm eating."; end; }; gotfood = function(w) take 'food'; -- this is a savegame stored variable food._num = w; back(); end; povardlg = dlg { nam = 'In the kitchen'; dsc = [[I notice the round face of the cook under a big white hat. She looks really, really tired.]]; obj = { [1] = phr("Some greens, please.", "Enjoy...", [[pon(); gotfood(1);]]), [2] = phr("Fried potatoes now!", "Bon appetit, ya filthy animal...", [[pon(); gotfood(2);]]), [3] = phr("Two bowls of garlic soup", "That's my favorite too!", [[pon(); gotfood(3);]]), [4] = phr("Something light, please. I've got an ulcer...", "Porridge for you then.", [[pon(); gotfood(4);]]), }; };
The player chooses her meal, she receives it and the nam
is stored in food._num
, then returns back to the room where she began the dialogue. Reactions can contain any Lua code, but usually it's phrase management logic.
NOTE: The pon/poff/prem/pseen/punseen
methods only work with indexes here. Deprecated dialogues CANNOT employ tags.
To pass from one deprecated dialogue to another, you would hide phrases while initializing the dialogue then unhide them later through the conditional _phr
variable and pon/poff/prem
methods, like so:
facectrl = dlg { nam = 'face control'; dsc = 'I see the unpleasant and unfriendly face of the fat security guy.'; obj = { [1] = phr("I've come to listen to the lecture of Belin...", [["I don't know you, -- the guard grins -- and I was told to only let decent people in."]], [[pon(2);]]), [2] = _phr("I have the invitation right here!", [["I see. Now look at that mirror there. I suppose you may not have one in your home. You've come to listen to Belin. Be-lin! The right hand of... - the guard pauses for a second - get out of here!"]], [[pon(3,4)]]), [3] = _phr("I'm gonna kick you in your fat face!", [["Well, let's see it." - powerful hands push me into the corridor, but I feel lucky to remain in one piece...]], [[poff(4)]]), [4] = _phr("You boar! I told you I have an invitation!", [["Do you?" - the guard's eyes turn red - a hard knock sends me out - it could be worse...]], [[poff(3)]]), }; exit = function(s,w) s:pon(1); end; };
NOTE: Again, do not write dialogues like this! Extended dialogues are much easier and better.
For style/aesthetic purposes you can define the prefix placed in front of every phrase, which is its numerical index by default:
stead.phrase_prefix = '--' stead.phrase_prefix = '+' stead.phrase_prefix = ':)'
Each of these causes all phrases to be prefixed by the given string instead of their index number.
This constant (stead.phrase_prefix) is not variable and so it must be defined early in game. Or you can set it in init()
function.
NOTE: The string value isn't stored in the player's savegame file, so the game has to define it explicitly for each playthrough. Be aware of this.
The game's graphic interpreter (the part that displays things onscreen) will treat a room's pic
attribute as a path to an image that should be displayed in the viewer.
home = room { pic = 'gfx/home.png', nam = 'at home', dsc = "I'm at home", };
Of course, pic
can also be a function. If the current room has no pic
attribute defined, the game.pic
attribute is assessed instead. If it's also undefined, no image will be displayed.
From version 0.9.2
you can use animated gif files, and graphics can be embedded everywhere, including within the text itself and inside the inventory panel useing the img()
method.
knife = obj { -- the nam will be a concatenation of the two nam = 'Knife'..img('img/knife.png'), }
In the current version you're free to use the disp
attribute instead of the nam
attribute.
knife = obj { nam = 'Knife'; ... disp = 'Knife'..img('img/knife.png'), }
Starting with version 1.3.0
text flow around images is supported. Use the imgl()
and imgr()
methods, which insert the image at the left or right border respectively. To add open space around the image use the pad:
prefix.
NOTE: The text flow images (imgl and imgr) cannot be a link.
Notice the abbreviated versions below and how they affect more than one side.
-- the image is padded on top 0px, right 16px, bottom 16px, left 4px imgl('pad:0 16 16 4, picture.png') -- the image is padded on top 0px, right 16px, bottom 0px, left 16px imgr('pad:0 16, picture.png') -- the image is padded on all sides with 16px imgl('pad:16, picture.png')
A clever use of the img()
method is to create blank areas and border boxes.
dsc = img 'blank:32x32'..[[Line with blank image.]]; dsc = img 'box:32x32,red,128'..[[Line with red semi-transparent square.]];
Starting with version 1.0.0
the interpreter can compose images from a base image and an overlay.
pic = 'gfx/mycat.png;gfx/milk.png@120,25;gfx/fish.png@32,32'
The interpreter cycles music that's been called with the set_music()
method, which is often implemented in the enter
handler.
street = room { pic = 'gfx/street.png', enter = function() set_music('mus/rain.ogg'); end, nam = 'on the street', dsc = 'It is raining outside.', };
The get_music()
method returns the name of the current track.
Starting with version 0.7.7
the set_music()
function can receive an additional parameter for the number of loops you wish it to play.
You can ask for the current loop position with the get_music_loop()
method. If -1 is returned it means the current track is finished looping.
From version 0.9.2
the set_sound()
method will play a sound file.
The get_sound()
method returns the filename of the sound that's queued to be played.
Starting with version 1.0.0
you can simply call stop_music()
to end the track being played.
From version 1.0.0
you can use the is_music()
method to check if music is playing or not.
The SDL version's graphic interpreter has a fancy theme subsystem. It accesses your game's theme
directory which must house a theme.ini
file. It must also include a default
theme. This is always the first to load. All other themes inherit from it and can partially or completely override its parameters.
Themes can be selected by the player in the settings menu, but your game can initially inject its own theme which will override all others by as much as you choose. To do this, place a duplicate of the theme.ini
file within the game directory, where main.lua
resides. The player can always override your theme if she wants. If she does, the interpreter will issue a warning that she's overruling the godlike developer's creative intentions. ;)
You can include comments with tags inside the theme header. Currently there's only one tag available. Here's a sample theme to show the basic format to follow:
; $Name:New theme$ ; modified "book" theme include = book scr.gfx.h = 500
The interpreter searches for the player's own themes in her installation's themes
directory. The Unix version also checks ~/.instead/themes/
and since version 0.8.7
the Windows version checks Documents and Settings/USER/Local Settings/Application Data/instead/themes
The theme.ini
file has a very simple syntax:
<parameter> = <value>; comment
A value can be a string, a number, or a color. Certain colors can be called with a string
, such as yellow
, green
, and violet
, but other colors must be written in #RGB
format, where R
, G
, and B
are replaced with hexadecimal values.
Parameters
scr.w
–game window width in pixels
scr.h
–game window height in pixels
scr.col.bg
–background color of the entire game window
scr.gfx.bg
–directory path to the static background picture, gifs are not enabled
scr.gfx.mode
–sets the layout mode for room pictures as float, embedded, or fixed
float
– the picture will be centered within scr.gfx.x
, scr.gfx.y
and downscaled to fit inside scr.gfx.w
, scr.gfx.h
embedded
– the picture is made part of the main story panel, and scr.gfx.x
, scr.gfx.y
, and scr.gfx.w
are ignored so that it can scroll with the textfixed
– same as embedded, except it doesn't scroll with the text
scr.gfx.x
, scr.gfx.y
, scr.gfx.w
, scr.gfx.h
–X/Y coordinates, width, and height of room pictures, adjusted by layout mode
scr.gfx.scalable = [0|1|2|4|5|6]
Added in Version 2.2.0
win.gfx.h
–synonymous with scr.gfx.h
(for compatibility)
win.x
, win.y
, win.w
, win.h
–X/Y coordinates, width, and height of the main text area only, excluding the scrollbar width, and the text will be pushed by a fixed or embedded room picture
win.fnt.name
–path to a font file
win.fnt.size
–font size for the main text area
win.fnt.height
–line height for the main text area, as a float number, 1.0 by default
win.align = left/center/right/justify
– alignment of text in the story panel's text area
win.gfx.up
, win.gfx.down
–paths to the pictures of up/down scroll handles for the main story panel
scr.gfx.pad
–sets padding for all scrollbars and menu edges
win.up.x
, win.up.y
, win.down.x
, win.down.y
–coordinates for scroll handles, literal position or -1 for invisible
win.col.fg
–font color for the main story panel
win.col.link
–color of clickable words in the main story panel
win.col.alink
–hover-over color for clickable words in the main story panel
win.scroll.mode = [0|1|2|3]
– the scrolling area on the scene
inv.x
, inv.y
, inv.w
, inv.h
–X/Y coordinates, width, and height of the inventory panel
inv.mode
–horizontal
or vertical
inventory stacking mode
–in vertical mode only 1 object fits per row, in horizontal multiple objects can fit
–you must reposition and resize the panel to make good use of horizontal
mode
inv.col.fg
–text color for the inventory panel
inv.col.link
–color for clickable words inventory panel
inv.col.alink
–hover-over color for clickable words in the inventory panel
inv.fnt.name
–path to a font file for the inventory panel
inv.fnt.size
–font size for the inventory panel
inv.fnt.height
–line height for the inventory panel, as a float number, 1.0 by default
inv.gfx.up
, inv.gfx.down
–paths to the pictures for the inventory panel's up/down scroll handles
inv.up.x
, inv.up.y
, inv.down.x
, inv.down.y
–coordinates of scroll handles, literal position or -1 for invisible
menu.col.bg
–background color for menus
menu.col.fg
–text color for menus
menu.col.link
–color for clickable menu items
menu.col.alink
–hover-over color for clickable menu items
menu.col.alpha
–transparency for menus as an alpha value, from 0-255
menu.col.border
–border color for menus
menu.bw
–border width for menus
menu.fnt.name
–paths to font files for menus
menu.fnt.size
–font size for menus
menu.fnt.height
–line height for menus, as a float number, 1.0 by default
menu.gfx.button
–path to the menu icon picture file
menu.button.x
, menu.button.y
–coordinates for the button location on the main screen
snd.click
–path to a click sound file
include
–path of a theme file to serve as filler for missing parameters, no extension or = sign needed
For example, this will build your custom parameters over the Book theme that comes included with Instead:
include book
The following parameters have been available since version 0.8.9
of the engine:
scr.gfx.cursor.x
–X coordinate of the center of the cursor
scr.gfx.cursor.y
–Y coordinate of the center of the cursor
scr.gfx.cursor.normal
–directory path to the cursor's picture file
scr.gfx.cursor.use
–path to a picture file for the cursor's use
indicator
scr.gfx.use
–path to a picture file for the room's use
indicator
Starting with version 1.2.0
you can employ feature extension modules via the require
method. Modules must be manually added to your main.lua
file, usually near the top.
... require "para" require "dbg"
These are the currently available modules:
dash
— prints middle dash instead of a normal onehotkeys
— you can set hotkeysquotes
— prints curly quotes (« and ») instead of boring onesdbg
–enables the debugging module (F7), which outputs to an external consolehideinv
–hides inventory interactions from clicks inside the story text areawalk
–enables an improved implementation of passagesxact
–permits multiple references to objects, improving hotlinksinput
–enables keyboard input, allowing you to implement simple text entry fieldskbd
–not as low-level as the input
moduletimer
–provides a timerclick
–enables mouse clicks on images to be capturedvars
–enables definition of variablesprefs
–enables savegame preferences and achievementssnapshots
–enables snapshotssprites
–enables spritesformat
–allows you to format output; by default all settings are disabledobject
–enables improved objectstheme
–enables theme manipulation on-the-flypara
–adds indentation to paragraphs
If the engine is higher than version 1.2.0
the 'vars', object
, and walk
modules are automatically activated.
To illustrate the use of modules in code, let's take a look at the prefs
module. A special object variable called counter
is part of this engine extension. It can store game variables such as player progress or number of attempts.
require "prefs" ... prefs.counter = 0 --number of times the player has attempted a room ... exit = function(s) -- the number of exits is incremented prefs.counter = prefs.counter + 1 -- saves the counter inside the prefs object prefs:store() end; ... enter = function(s) -- retrieves the counter and displays it return 'You passed the game '..prefs.counter..' times'; end; ... act = function(s) -- clears the prefs object prefs:purge() return "Preferences has been cleared" end;
WARNING: The following examples utilize deprecated syntax, and are included in this documentation for the sake of potentially helping you sort your way through obsolete code in old games.
Please, use module kbd
to handle keyboard. Also, there is a module keyboard
to make possible the player text input.
Since version 1.1.0
the SDL version has supported real-time keyboard input. This can be activated via the input
object.
input.key(s, pressed, key)
The first argument references the keyboard handler's self. The second argument accesses the engine's press and release events. The third argument receives the name of the specific key being activated.
The input object's key
handler can also return an engine interface command, in which case the interpreter won't handle it, which is useful for behind-the-scenes effects. For example:
input.key = function(s, pr, key) if not pr or key == "escape" then --checks if the Escape key was activated return --exits the current typing area?? elseif key == 'space' then --checks if the Spacebar was activated key = ' ' --outputs an empty space through the key handler elseif key == 'return' then --checks if the Enter key was activated key = '^'; --outputs a line break through the key handler end if key:len() > 1 then --checks if the return end main._txt = main._txt:gsub('_$',''); main._txt = main._txt..key..'_'; return "look"; end; main = room { _txt = '_', forcedsc = true, nam = 'Keyboard', dsc = function(s) return 'Example: '..tostring(s._txt); end; };
You may use click
module to handle mouse click events.
Module will call here().click or game.click method with coordinates (x, y) of click in parameters.
0,0 is the left top corner. You can get even background clicks, just define:
click.bg = true;
So, the method will called in this case with those parameters:
game.click(s, x, y, px, py)
Where x, y is the coordinates of whole window and px,py - coordinates in image (if image was clicked).
If you need to get press and release events, you may define:
click.press = true;
In such case the parameters are:
game.click(s, press, x, y, px, py)
Where press is boolean.
If you need to get button number, please, define:
click.button = true
And get the button number as the 3d parameter.
Here is the example of the module usage:
-- $Name: Tets module -- click$ -- $Version: 0.1$ -- $Author: instead$ instead_version "1.8.0" -- use module require "click" -- define game.click game.click = function(s, x, y) -- this is global method, -- will called if not here().click defined p ("You are clicked at: ", x, ", ", y); end main = room { nam = 'Forest', forcedsc = true, pic = 'house.png', dsc = [[ You are seeing a house. Door is open. ]], -- define own click method in this scene click = function(s, x, y) -- is this the door area? if x > 80 and x < 200 and y > 225 and y < 325 then walk('house'); else return 'This is not the door.'; end; end, }; house = room { nam = 'Дом', forcedsc = true, pic = 'door.png', dsc = [[ You are inside the house.]], };
To implement a personal music device for the player to control you must create it as an “alive” object. First create a tracklist in the tracks
handler.
--might be an attribute... not sure tracks = {"mus/astro2.mod", "mus/aws_chas.xm", "mus/dmageofd.xm", "mus/doomsday.s3m"}
Then create a living, breathing Walkman object to entertain your players. ;)
mplayer = obj { nam = 'media player', 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');
You can even use get_music_loop()
and get_music()
to remember the last track and let the player restore it.
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; ...
Since version 0.8.5
the save_music()
and restore_music()
methods have been permanently included in the game library.
If your player lost their horse or they need something else to appear next to them, one way to materialize that object is to implement the lifeon()
method as a teleporter, controlled by the object's life
handler. This will materialize the object at the player's current location, technically just bringing it to life again. But you get the idea.
horse = obj { nam = 'horse', dsc = 'A {horse} is next to me.', life = function(s) if not seen('horse') then move('horse', here(), s.__where); s.__where = pl.where; end end, }; function init() lifeon('horse'); end;
Menus are a great feature to add to your world. You can add them to an object by incorporating the following menu
constructor into your code. It's activated by the player with a single mouse click. If it has no return string, the state of game will not change. For example, here's a simple implementation of a pocket housed inside the inventory.
pocket = menu { var { state = false }; nam = function(s) if s.state then return txtu('pocket'); end return 'pocket'; end; gen = function(s) -- this is our custom function -- pocket generator if s.state then s:enable_all(); else s:disable_all(); end end; menu = function(s) s.state = not s.state s:gen(); -- generate pocket end; }; knife = obj { nam = 'knife', inv = 'This is knife', }; function init() inv():add(pocket); place(knife, pocket); pocket:gen(); end; main = room { nam = 'test', };
Another cool implementation of menus is to place an unclickable text readout of the player's status within the inventory panel.
global { life = 10; power = 10; } status = stat { -- create status object nam = 'my status'; disp = function(s) p ('Life: ', life, 'Power: ', power) end; }; function init() take 'status'; end;
Split your game into multiple files to make it easier to manage. You can use the dofile()
method to insert your separated source code files into the game. You must use dofile()
in a global context, so load all of them while parsing main.lua
at the opening of the game.
...somewhere inside main.lua... dofile "episode1.lua" dofile "npc.lau" dofile "start.lua"
Dynamically including files gives you the ability to redefine objects and rooms as the game progresses. Nifty! Just use the gamefile()
method.
act = code [[ gamefile("episode2.lua"); ]]
You can also clear the game stack of old files, leaving you with a brand new game without the player needing to load anything herself. This is quite useful for multi-part stories where locations need to be revisited in a brand new light. Just put true
as a second parameter for the gamefile()
method.
act = code [[ gamefile("episode3.lua", true); ]]
Since version 0.9.3
, if you want to hide your source code you can encode it from the command line as follows:
sdl-instead -encode <lua file> [encoded file]
Then to load your encoded file from Lua use:
doencfile()
It's necessary to keep main.lua
as a plain text file for this, so the recommended scheme is:
game is a encoded game.lua ): main.lua -- $Name: My closed source game!$ doencfile("game");
WARNING: Do NOT use the luac compiler for encoding, as it produces platform-dependent code! For regular game compilation it's quite useful for hunting down bugs, but not for encoding.
Since version 1.4.0
you're able to package all your game's resources including graphics, sounds, themes, etc., into one fancy .idf
file. Just put your resource directories all into a single directory called data
and from the command line run:
instead -idf <path_to_your_parent_data_directory>
A file called data.idf
will be created. Put this into your game's top level directory along with the code files and remove the original resource files.
You can also pack your entire game into one .idf
file like so:
instead -idf <path_to_game_directory>
A game stored in .idf
format can be run like any other game within INSTEAD, or straight from the command line with:
instead game.idf
Translation from Russian by Vopros
English Localization and Technical Editing by Ryan Joseph