Donation

Help us to pay our server!
(: Consider a donation :)



Social Media

  • Facebook
  • Google+

Latest Posts

Scenario Tutorial

This article works only with a widelands version > build 19

This tutorial has the goal to give some initial help for creating a scenario. While we can't provide a full explanation of the Lua language, nor all the possibilities of the Lua API that widelands provides, you should be able to understand the code of the implemented scenarios and adapt it to your own scenario.

When you have reached the end of this tutorial, you will have learned how the implemented scenarios are structured, how to create message boxes, make a smooth scrolling of the map, create a logic when the player reaches a specific point on the map, give the player an objective to build some buildings and create the logic for the objective.

What you should do before reading on:

  • Read Scenario Tutorial, at least the chapters 'Designing a map' and 'Hello World'. This tutorial explains some of the LUA language, but is not a complete LUA manual. Where appropriate, external pages are linked, so the reader can get a deeper look into LUA.
  • When writing scripts, make sure to use a plain text editor that can handle UTF-8 encoding, e.g. notepad++, Geany or dig through the comparison of text editors on wikipedia. Do not use default Windows Notepad or Wordpad.
  • Users of a Windows OS should know how to start widelands from a command line. See How do I get log output in Windows for debugging?

Preparation

Uncheck the option "Compress widelands datafiles" in the options menu. Then create an empty map with only one player set at position 0, 0. To find this position, watch the values at the bottom right in the editor. Those values represent: x position, y position, height. Make sure you have set the tribe 'Empire' in the menu 'Players Option'. After saving this map, a new directory (folder) with the name of the map is created in the folder 'maps/_My_maps'. If you have trouble finding this directory, see: Where are the maps and savegames stored.

Use a filemanager, navigate to the folder of the map and create a new directory called 'scripting/' inside it. The folder 'scripting/' will contain all the needed files for the scenario. The main file for a scenario is called 'init.lua'.

init.lua

The file 'your_map/scripting/init.lua' will be called by Widelands when a new game is started on your map, and as such it is the main entry for the scenario. This file should contain necessary global settings, which are usually separated into different parts.

Including some helper scripts

Add these lines to 'init.lua'. Windows users should not be confused about the forward slashes (/). All code inside files needs forward slashes for file paths. The only place where windows users needs backward slashes () are on the command line when starting the scenario.

include "scripting/coroutine.lua" 
include "scripting/messages.lua"
include "scripting/ui.lua"
include "scripting/richtext.lua"

The files which get included here contain functions which you may need in your project. E.g. the file 'coroutine.lua' contains a function sleep(time). Those files (and their functions) are described in Auxiliary Scripts.

Setting Global Variables

Below the includes in 'init.lua', define the variables which can be used in the following files. That's why they are called 'global' variables. These global variables are shortcuts and remove the need to rewrite them:

-- Defining global variables
plr = wl.Game().players[1]
map = wl.Game().map
plr_sf = map.player_slots[1].starting_field

What this code does:

Line Explanation
-- Defining global variables All text beginning with two hyphens is treated as a comment, so this does nothing. You can also out comment parts of lua code to deactivate it - this is useful for testing
plr = wl.Game().players[1] This reads the first defined player of the map (the one you have set) and assigns it to the variable 'plr'. If you have set more players to the map, they can be accessed by increasing the value in the brackets, so wl.Game().players[2] is the second player.
map = wl.Game().map The variable map gives you access to many properties of the map itself, e.g. to the function get_field(). This will be the most used function in your scenario.
plr_sf = map.player_slots[1].starting_field You have set a player in the map editor on a field, so this assigns this field to the global variable plr_sf. Note that the variable map which was defined one line above is already being used here. Also note that there is a dot between 'map', 'player_slots' and 'starting_field'. I tell you later why this is important.

Including the script to load starting conditions

Now the main initialization is done and you can work on getting the story alive. To separate initialization and other stuff from each other, include another file in the last line of your 'init.lua':

-- Load own lua scripts
include "map:scripting/starting_conditions.lua"

Note that the path in this include starts with map:. This is a shortcut to your map's directory, wherever it might be. In our example, it points to 'maps/_My_maps/your_map/'.

The file 'starting_conditions.lua' will contain initial settings for the players. The include at the end of the file 'init.lua' makes sure all other initialization is done, and the global variables are available in each further included file.

starting_conditions.lua

The map you have created is empty by default. You have set a player, but no buildings at all. It is your part to place buildings and fill them with wares and workers.

If not already done, create a new file and save it as 'starting_conditions.lua' in the folder 'scripting' of your map.

Place buildings

First you place the headquarters for the player. Do you remember you have set the global variables plr and plr_sf? Here we use them:

-- Place the headquarter for player1
local hq = plr:place_building("empire_headquarters", plr_sf, false, true)

Compare the given arguments with the description of place_building(). The first argument for the function is a string (enclosed in double quotation marks). You could find the correct string (the name) in the corresponding init.lua of the building in the subfolders of 'data/tribes/buildings/' or open the Encyclopedia and check the Option "Show names for scripting".

The above function returns the building of the created buildings. In this example the building created is stored in the local variable hq.

Importand remark about local: Whenever you assign variables with values, you should do it this way: local name_of_variable = value/function. The keyword local makes sure that this variable is only valid in this scope (file or function). The only exception: If you really know that you will use this variable elsewhere, you can omit the keyword local. But in this case you should give the variable a name which is absolutely clear!

Note the colon between plr and the functions name. Do you remember that there where already a point as a separator? Time for a small explanation about the difference between point and colon:

Consider to have an object like a car. This car has a property: colour. But this car has also some functions: drive_straight(), drive_left(), drive_right(). If you want to get the colour, you want to access the property: clr = car.colour. If you want to drive the car you need to call the functions: car:drive_straight(100 meter). Do you see the difference? Accessing a property needs a point, calling a function needs a colon! If you go through the documentation you may notice that some entries have brackets, and others not. The entries with brackets are functions, other are properties. Examples:

Now its time for a first test. Run:

widelands --scenario=/full/path/to/the/map.wmf
TODO: example for windows OS

You should see the headquarters placed after loading is complete. Look at the stock... the headquarters is empty, no wares and workers are in it. You should change that.

Fill buildings

The function place_building() returns the building of the created building. The building is stored in the local variable hq and this variable gives access to the building itself. So you could use the function set_wares() to fill the building:

hq:set_wares("log", 5)
hq:set_wares("planks",10)
hq:set_wares("granite",10)

Because setting wares is a common task, there is a shortcut by passing a table with 'key = value' pairs to set_wares():

hq:set_wares({
   log = 5,
   planks = 20,
   granite =  20,
   marble = 5,
})

The implemented scenarios are a bit different in syntax, see Assigning tables to functions.
For an introduction to tables, see the Tables tutorial in the lua users wiki.

Same goes for setting workers:

hq:set_workers({
    empire_builder = 5,
    empire_lumberjack = 5,
    empire_forester=2,
})

Like with the buildings name you will find the right names for wares and workers in the corresponding 'init.lua' in the subfolders of 'data/tribes/' or in the Encyclopedia with the option "Show names for scripting" turned on. Add as many wares and workers as you like.

Setting soldiers is some what complicated, because you have to define also the soldiers training levels. See set_soldiers() for an explanation of the values.

hq:set_soldiers({0,0,0,0}, 45)

After finishing these settings you should make a test run again.

Summary

  • A campaign starts always with an empty map, but needs setting the starting position of players including their tribe
  • The file 'init.lua' contain only basic initial settings
  • Do the players initialization in the file 'starting_conditions.lua'
  • Its up to the scenarios author to place at least the basic buildings (headquarters, warehouses). AI players needs at least a headquarters or a warehouse.
  • Also the initial set of wares, workers and soldiers is up to the scenarios author
  • Use always the keyword local when defining variables
  • Include 'starting_conditions.lua' at the end of 'init.lua' so the contents will be available

Now its time to create the main control script. It is usually called 'mission_thread.lua'

mission_thread.lua and texts.lua

The file 'mission_thread.lua' contains most of the logic for the scenario. The file 'texts.lua' should contain texts for message boxes that your scenario needs.

Create at least the file 'mission_thread.lua'.

Message boxes

You may have seen that our scenarios have some message boxes to inform the player about the story. In 'init.lua' the functions of 'messages.lua' are included. This file contains convenient functions for messages. Use campaign_message_box() and add the following to the new file 'mission_thread.lua':

campaign_message_box({title = "This is the title", body = p("Hi, this is my first scenario")})

The function is called with the needed arguments, in this case a table (enclosed with curly braces {}). The table consist of two needed keys: 'title' and 'body'. The value for 'body' must be formatted with the richtext system, the function p(text) is used here. When adding an objective you will learn how to use other formatting functions.

The file 'mission_thread.lua' must be loaded, so include it as last line in 'init.lua':

-- Load own lua scripts
include "map:scripting/starting_conditions.lua"
include "map:scripting/mission_thread.lua"

Run a test. The message box should be shown after loading has finished.

texts.lua

You may have more than one message box in your campaign. For better readability of your main script 'mission_thread.lua' it is better to put the table(s) of text in an extra file. The built in campaigns have them usually in a file called 'texts.lua' which get included in 'init.lua' after including 'starting_conditions.lua' or at the beginning of 'mission_thread.lua'. This will look then like:

File 'texts.lua':

-- Store the table in the global variable 'first_message'
first_message = {
   title = "This is the title",
   body = p("Hi, this is my first scenario"),
}

The implemented scenarios use a different syntax for the value of 'body', see Strings and localization

File 'mission_thread.lua':

-- Including tables of text so the variables defined over there can be used here
include "map:scripting/texts.lua"

-- Use the variable (which represents the table in texts.lua)
campaign_message_box(first_message)

You could also modify the message box, by applying additional key/value pairs to the table in 'texts.lua':

-- Store the table in the global variable 'first_message'
first_message = {
   title = "This is the title",
   body = p("Hi, this is my first scenario"),
   posx = 1,
   posy = 1, 
}

See message_box() for the explanation of posx and posy.

Exercise:

  • Play around with the values of posx and posy.
  • Change the width and height of the message box

Your code can look like shown here

Triggering events - adding logic

You know now how to create message boxes, but you will also need an event, a trigger, to show it to the player. Such events are the most important things in campaigns. They offer functionality like:

  • Show message boxes when things happens
    • Running out of building material
    • The player has reached a specific point on the map
    • A specific building has been build
  • Add objectives a player has to solve
  • and so on

Usually an event is combined with an objective, but because triggering an event is an important task, objectives are handled in the next chapter. In this example an event is created if the player reaches a specific point on the map. There are two possibilities to get this work:

  • Checking the return value of the function Player:sees_field(field). A player sees a field, if it is revealed. It is not necessary if this field is in the displayed area of the map (a player currently sees on his display). This field could be in the players territory, or not.
  • Checking the property Field.owner. A owned field is inside a players territory.

In this example the property field.owner is used, but both possibilities needs a field to work with. Fields are part of the map and can be accessed through wl.Game().map. In the global variables section of init.lua this was assigned to the variable map . Now this variable is used to call the function get_field(x,y). This function needs the x and y coordinates of the map, so add the following to 'mission_thread.lua' below the call to campaign_message_box:

-- Get the field at position x=10, y=0 and store it in the variable 'field'
local field = map:get_field(10,0)
print("Field: ", field , "Owner: ", field.owner)

If you run the scenario the field coordinates and the owner of that field will be printed in the console. The value of field.owner is currently 'nil', because this field has no owner. Note that the print statement is only executed after clicking "OK" in the message box.

What is needed further is a mechanism to check if the owner of that field has changed. A loop is needed that runs the same code over and over until the owner has changed. The problem with such loops is that they can't be run as a standalone loop in LUA. This is where coroutines comes in. A coroutine is started by calling the function run(). So you create a loop inside a function and call run() with the functions name as argument at the end of 'mission_thread.lua'. Replace the previous added code with this one, note the moved message box:

-- Defining a function called 'check_field_owner'
function check_field_owner()
    local field = map:get_field(10,0)

    -- Start defining a loop
    while true do
        print("Field: ", field , "Owner: ", field.owner)
        sleep(1000)     -- An important row, see below
    -- End of defining the loop
    end

-- End of defining the function
end

-- Call the function 'run()' with name of function as argument
run(check_field_owner)

campaign_message_box(first_message)

The row sleep(1000) is really important. If you forget that, this loop prevents every interaction between the player and the program itself:

  • A player can't do anything else while the coroutine is executed
  • The only way to stop it is to kill the whole program either by clicking on the 'X' of the window, or with your taskmanager

Run the scenario and look into the console output. After loading is complete, the field coordinates and the field owner is printed out exactly one time. Then the message box appears. If you click "OK" in the message box the print statement is further called every second. The important thing with this observation is:

Starting a coroutine returns immediately! : After starting the coroutine by calling run(name_of_funtion) all code after this call will be executed also. In this case: The message box is shown. So it is possible to start coroutines by calling run(name_of_funtion) more than once.

Ok, now it's time for some magic: Run your scenario and explore into the direction of the field (to the right). As soon the field is owned by you, the output will change. Why is this magic? The variable field is defined outside of the loop so it is initialized only once. One could think that the fields state (the owner in this case) is also only initialized once, but this is not the case. That is because the variable field is a reference to the field. Whenever this field changes the changes are also reflected in the variable field!

Exercise:

  • Add a print statement close before the loop starts, e.g.: print("Starting loop")
  • Add a print statement after the loop, e.g.: print("The loop has been ended")
  • Run the game and watch the consoles output. When will the print statements be shown in console?

Solution for adding print statements

Starting a coroutine calls the function, but the function is not executed entirely!: The execution of the function is 'caught' in the loop. All code after the loop isn't executed until the loop will end.

After this exercise you have seen that the third print statement is never shown, the loop created above will never end. This has to be changed by adding a condition to the loop. You may have seen in the consoles output, that the field.owner has changed to Player(1) after expanding the territory to the right. The first player was assigned to the global variable plr in 'init.lua', so comparing field.owner with the global variable plr should do the trick. To check that the loop is really endet, another message box is added:

function check_field_owner()
    local field = map:get_field(10,0)

    -- Run loop as long 'field.owner NOT EQUAL plr'
    while field.owner ~= plr do
        sleep(1000)
    end
    campaign_message_box( {title = "Field owned", body = p("This field is now owned by you!")} )
end

run(check_field_owner)

campaign_message_box(first_message)

The loop runs now as long 'field.owner not equal plr'. Such a comparison returns either 'true' or 'false'. See also Control Structure Tutorial in the Lua users wiki.

As soon the message box appears, you can be sure that the loop has been ended.

Exercise:

  • Move the table of the added message box into the file 'texts.lua' and make the corresponding changes
  • Add a new function check_sees_field() and use sees_field() for the condition. You have to choose another field though, one which is outside the area the player sees, for testing purposes use field 15,0
  • Create another message box which shows that the player can now see the field
  • Run the new function by adding an additional run() command

When running the scenario and you explore the territory to the right, you should see at least two message boxes popping up: One after the first coroutine has ended, and the new one after the second coroutine has ended.

Did you get it to work? Congrats! If not, here is the Solution.

There is one drawback with coroutines: If you save the game, the coroutines and the state of their variables are saved also. This means you can't run a game, save it, change the logic of a coroutine, and reload the saved game to check the new logic of the coroutine. The new logic will not be part of the loaded save game, because it uses the old logic. The only way to test the new logic is to start a new game. This makes working on scenarios a bit hard, especially for beginners. At the end of this article are some tips for testing coroutines

Summary

If you have reached this point of this tutorial you have learned a lot. Here are the main things:

  • Split logic and texts und leave logic in 'mission_thread.lua' whereas put texts in other files
  • Include each other file either from 'init.lua' or at the beginning of 'mission_thread.lua'
  • Add events as functions and execute them through run(function)
  • Don't forget to use sleep() in loops
  • To check a procedure or verify variables you can always use a print() statement
  • Changing logic of coroutines have only affect if you start a new game

Smooth scrolling of the map

The included file 'ui.lua' contain the needed functions for smooth scrolling. The first function is wait_for_roadbuilding_and_scroll(field), which returns the center x/y position in pixels of the map. You store the return value in a local variable, here cur_view, and use this variable later on for the function scroll_to_map_pixel(pixel_coordinates). Apply this to the function check_field_owner():

function check_field_owner()
    local field = map:get_field(10,0)
        while field.owner ~= plr do
        sleep(1000)
    end

    -- Call the function and store the return value
    local cur_view = wait_for_roadbuilding_and_scroll(field)
    campaign_message_box(msg_field_owned)

    -- Use the return value to scroll back to 'cur_view'
    scroll_to_map_pixel(cur_view)
end

Run a test and watch the result.

Exercise:

  • Add smooth scrolling to the function check_sees_field()

Objectives

If you have played some of our scenarios you have seen that there are objectives a player has to fulfill. The steps for objectives are similar to what you have learned so far when triggering events:

  1. Show a message box with the objectives text and add it to the game
  2. Run a loop until the objectives conditions are fulfilled
  3. Remove the objective from the list of objectives

At the end of this chapter, the objective should be started close after the player has owned the field and should advise him to build a forester, a lumberjack and a sawmill.

Add an objective

The function add_campaign_objective() is responsible to add an objective to the game, so it is shown when hitting the Objectives button in the bottom menu. Try it out by adding this code at the end of 'mission_thread.lua' (so it is executed directly after the coroutines has been started):

-- Testing adding an objective
local obj = add_campaign_objective({
    name = "build_basic_economy",
    title = "Build a forester and a Lumberjack",
    body = li("Build a forester") ..
           li("Build a lumberjack"),
  })

Run the game and hit the button to show the objective.

The code is similar to showing a message, but there is one additional key in the table: name. The value for name is important and is used internally to distinguish between the added objectives. When ever adding an objective, make sure the value assigned to name is unique for the whole scenario. The 'body' is formatted with function li("text"), which shows the texts as list items. The doubled points (..) is the Lua string concatenation operator and must be used to concatenate all strings. See richtext.lua for other formatting functions.

Exercise:

  • As with message boxes it is better to separate texts and logic, so pull out the table and store it in 'texts.lua'.
  • Add another list item to the objective saying to build a sawmill

Solution

The logic of the objective

The needed function to get the build buildings of a player is check_for_buildings() and lives in the file 'objective_utils.lua'. This file isn't included yet, so include it. Then create a function for the objective and use the return value of check_for_buildings() in the loop for the comparison. You have to add the texts for the additional message boxes yourself:

-- The coroutine to check if the condition of the objective is fulfilled
function check_basic_economy()
    campaign_message_box(msg_ask_for_basic_economy)
    local obj = add_campaign_objective(obj_build_basic_economy)
    while not check_for_buildings(plr, {
                                empire_lumberjacks_house=1,
                                empire_foresters_house=1,
                                empire_sawmill=1,
                                }) do
        sleep(1000)
    end
    campaign_message_box(msg_done_basic_economy)

    -- Mark the objective as solved
    obj.done = true
end

Add an additional run() statement at the end of 'mission_thread.lua' to run the new coroutine and run the scenario. First check the the objective by clicking on the button 'Objectives' in the bottom menu, then build a forester, a lumberjack and a sawmill, then check the 'Objectives' again.

But this new coroutine is running right after start, but it should show up after we have owned the field. All what is needed to archive this, is to move the row run() from the end of 'mission_thread.lua' to the end of the function check_field_owner():

function check_field_owner()
-- ... existing code ...

   -- Start the objective now
   run(check_basic_economy)
end

Note that the game is automatically saved as soon the line obj.done = true is executed.

Combining message box and objective

The function to check the objective contains two message boxes: One before the objective is added to the game, and one at the end. It is possible to combine the first message box with the call to add the objective, so the code is one line smaller. The related function for this is message_box_objective() and is provided through 'messages.lua'. This function also has additional functionality, like showing the message box in the top right corner of the screen.

Change the table of the objective (in 'texts.lua') to this one:

obj_build_basic_economy = {
    -- Values for the message box
    title = "New objective",
    body = p("You should build a forester, a lumberjack and a sawmill."),
    w = 120,
    h = 120, 

    -- Values for the objective
    obj_name = "build_basic_economy",
    obj_title = "Build a forester and a Lumberjack",
    obj_body = li("Build a forrester") ..
               li("Build a lumberjack") ..
               li("Build a sawmill"),

    -- Additional arguments
    position = "topright",
}

This is similar to what we had already. The values for the message box are the same, but the attributes for the objective changed to have the prefix 'obj_'.

Exercise:

  • Remove the first message box in function check_for_buildings() and also the related code for it in 'texts.lua'
  • Use the new function message_box_objective() instead of add_campaign_objective(). Don't forget to add the first argument 'plr'

Solution

Summary

  • Formatted strings must be concatenated with the LUA string concatenation operator ..
  • Use either add_campaign_objective() or message_box_objective() to add objectives to the scenario.
  • The objectives attribute name must be set and unique for the whole scenario
  • If the objectives attribute done is set to true a savegame will be created automatically

Checking for a worker

You might have noticed that the objective is solved, but the Carpenter is missing in the sawmill. The function check_for_buildings() checks only if the building is build, nor if the needed worker is arrived at the building or if the worker is available at all. Of course this can be solved by adding a worker to the starting conditions, but this would be too simple.

This last example will guide you through other types of loops and control structures. At the end you have learned how to use if-statements and how to dig tables for values.

Get the players build buildings

For testing purposes you should do all the following at the end of 'mission_thread.lua'. This makes it easier to test the code because you didn't have to run the whole scenario until the sawmill is build.

First place two sawmills, then make a call to plr:get_buildings() with the buildings name to evaluate:

-- Place buildings with no worker
include "scripting/infrastructure.lua"
place_building_in_region(plr, "empire_sawmill", plr_sf:region(6), {workers={empire_carpenter=0,}})
place_building_in_region(plr, "empire_sawmill", plr_sf:region(6), {workers={empire_carpenter=0,}})

-- get the players buildings of type empire_sawmill
local buildings = plr:get_buildings("empire_sawmill")

The function get_buildings() returns either an Array or a table, depending on the given argument. In this case (applying one single string) the return value is an array. See the Tables Tutorial in the lua users wiki for an explanation about the difference between tables and arrays.

Before going on with the scenario, let's have a look at the variable buildings and which information can be extracted from it, by adding some print statements:

print("Type of variable buildings: ", type(buildings))
print("Number of buildings in this array: ", #buildings)

print("Looping over the array and print the key-value pairs:")
for key, value in ipairs(buildings) do
    print("Key: ", key, "Value: ", value)
end

print("A value for a key can also be accessed through 'array[key]'")
print("The value of key 1: ", buildings[1])
print("The value of key 2: ", buildings[2])

For an explanation of the for-loop, see the ForTutorial in the lua users wiki.

You may get confused about the terms table in the output. Where are the two sawmills? Well, the tables are the entry points to the functions defined in C++, so you can't directly examine the values for the tables. What you can do, is to call the properties and functions behind it, e.g.:

print("The field were the second building in this array was build: ", buildings[2].fields[1])
print("The internal name of the first building stored in the value of key 1: ", buildings[1].descr.name)
print("The type of the building: ", buildings[1].type_name)
print("The amount or carpenters in first building of the array: ", buildings[1]:get_workers("empire_carpenter"))

The for-loop strips out each 'key-value' pair and put their values in the variables key and value. Run the scenario and watch the output. Now change the call to get_buildings and make it return a real table (not a table array):

local buildings = plr:get_buildings({"empire_sawmill", "empire_headquarters"})

Run the scenario again. Your output of the two different calls to get_buildings() should look similar to this one (the numbers after 'table:' are memory addresses and different on each call):

get_buildings("empire_sawmill") returns an array get_buildings({"empire_sawmill", "empire_headquarters"}) returns a table
Buildings:      table: 0x56401fb83010
Number of buildings: 2
Key: 1 Value: table: 0x56401fb83050
Key: 2 Value: table: 0x56401fb830d0
Buildings:      table: 0x5592d9fdd110
Number of buildings: 0
Key: empire_headquarters Value: table: 0x5592db071210
Key: empire_sawmill Value: table: 0x5592d9fdd150
Counting the entries in an array can be done with a hashtag (#) Counting of entries in a table isn't possible
Keys of an array are always integers Keys of a table can be everything, also strings
The table 'buildings' has each sawmill in his own row The table 'buildings' has each building in his own row.
The two sawmills are stored in the table with key "empire_sawmill"

Solutions

Play with message box attributes

File 'texts.lua':

-- Store the table in the variable 'first_message'
first_message = {
   title = "This is the title",
   body = p("Hi, this is my first scenario"),

   posy = 100,
   posx = 100,

   -- Applying width and height to the message box
   w = 200,
   h = 200,
}

Add print statements

Your code should look like:

function check_field_owner()
    local field = map:get_field(10,0)

    print("Starting loop")
    while true do
        print("Field: ", field , "Owner: ", field.owner)
        sleep(1000)     -- An important row, see below
    end
    print("The loop has been ended")

-- End of defining the function
end

The Console output shows Starting loop and Field: Field(10,0) Owner: nil only once. After clicking "OK" in the message box, the lines Field: Field(10,0) Owner: nil is printed out every second. The third print statement is never shown!

Add another function

Does your solution look like this one?

-- Added function called check _sees_field
function check_sees_field()
   local field = map:get_field(15,0)
   while plr:sees_field(field) ~= true do       -- See below
      sleep(1000)
   end
   campaign_message_box(msg_sees_field)
end

run(check_field)

-- Additional run() command
run(check_sees_field)

The condition for the loop could also be written this way to improve readability:

   while not plr:sees_field(field) do

Separate objective texts and logic

It is usual to start the name of the table of an objective with the prefix 'obj_':

File 'texts.lua':

-- ===========================
-- Objectives for the scenario
-- ===========================
obj_build_basic_economy = {
   name = "build_basic_economy",
   title = "Build the basic economy",
   body = li("Build a forrester") ..
          li("Build a lumberjack") ..
          li("Build a sawmill"),
}

File 'mission_thread.lua':

-- ..old code ..

-- Use the table of texts.lua
local obj = add_campaign_objective(obj_build_basic_economy)

Usage of message_box_objective()

function check_basic_economy()
  local obj = message_box_objective(plr, obj_build_basic_economy)

-- old code goes here ..

Differences between implemented scenarios

Assigning tables to functions

The implemented scenarios sometimes left out the outer brackets when calling a function:

hq:set_wares({
   log = 5,
   -- ...
)}
-- Same as
hq:set_wares{
   log = 5,
   -- ...
}

For an explanation see calling conventions

Strings and Localization

The implemented scenarios use different markers for strings. Instead of enclosing long strings in double quotation marks, they are enclosed with double square brackets [[ string goes here ]]. Using double square brackets has some advantages for localization. See also Lua users wiki: Strings tutorial.

The Strings are also marked for localization with the function _() (an underscore).

Tips for testing coroutines

Writing and testing coroutines can get very hard if you have many of them. Especially if one coroutine depends on another. Because coroutines can only be tested by starting a new game, the latest coroutine in your scenario may need test playing the whole scenario. This could be very boring...

Commenting Coroutines

The easiest thing one can do to test coroutines is to out comment all other coroutines, so only the one you currently writing on is executed. This can be easily done by out commenting the run() command. If this coroutine depends on another one, consider to out comment also some logic.

Scriptconsole (only available in debug builds)

In debug builds a script console available, which you can open via the F6 key. In there you have access to all the global variables defined in 'init.lua' (e.g.: map or plr). Commands entered here have effect on the currently running game. Some hints for using:

  • Every output (print statements or errormessages) are printed into the console (from where you have started the scenario).
  • You can assign new variables: field = map:get_field(10,0)
  • Press 'Arrow up' to bring up the last entered command(s)
  • Commands have to be in one line, e.g.: for x=0, map.width - 1 do print("Field: ", x) end
  • Load a script from a file: dofile("path/to/filename.lua")

Discuss this article