Changes in WikiSandbox
Editor Comment
Don't use 'we'
Revision Differences of Revision 95
Test [[ wikisyntax | wiki syntax ]] here :-) ¶¶
[TOC] ¶
¶
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
¶
What you should know before reading on: ¶
¶
* [First steps for a scenario](https://wl.widelands.org/docs/wl/tutorial/) ¶
* Make sure to use a plain text editor when writing scripts, e.g. [notepad++](https://notepad-plus-plus.org), [Geany](https://www.geany.org) or [dig through wikipedia articles about text editors](https://en.wikipedia.org/wiki/Text_editor) ¶
* You may also take a look in the scripting files of the implemented campaigns, which you can find in the data directory of your widelands installation. Look into the folder data/campaigns. This article follows the same structure of filenames. ¶
¶
To follow this example, 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. This values represents: (x position, y position, height). Make sure you have set the tribe 'Empire' in the menu 'Set Player'. ¶
¶
## init.lua ¶
¶
You have read that the file `your_map/scripting/init.lua` is the main entry for your scenario. So this file is the starting point and should contain necessary global settings. This file contains usually different parts: ¶
¶
### Including some helperscripts ¶
¶
~~~~ ¶
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'_ contain a function `sleep(time)`. Those files (and their functions) are described in [Auxiliary Scripts](https://wl.widelands.org/docs/wl/autogen_toc_auxiliary/).
¶
### Global variables ¶
¶
In this part of the file _'init.lua'_
¶
~~~~ ¶
-- Defining global variables ¶
plr = wl.Game().players[1] ¶
map = wl.Game().map ¶
plr_sf = map.player_slots[1].starting_field ¶
~~~~ ¶
¶
What this code does: ¶
¶
* `-- Defining global variables`: All text beginning with two hyphens are treated as a comment, so this does nothing ¶
* `plr = wl.Game().players[1]`: This reads the first defined player of the map (the one you have set) and assign 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 `.players[2]` is the second player. ¶
* `map = wl.Game().map`: The variable 'map' gives you now access to many properties of the map itself, e.g. to the function `get_field()` in [Map](https://wl.widelands.org/docs/wl/autogen_wl_map/#wl.map.Map). 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
¶
### Including the script to load starting conditions ¶
¶
Now
¶
~~~~ ¶
-- Load own lua scripts ¶
include "map:scripting/starting_conditions.lua" ¶
~~~~ ¶
¶
The file _'starting_conditions.lua'_ will contain initial settings for the scenario. The include at the end of the file _'init.lua'_ makes sure all other initialization is done and all defined 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. ¶
¶
### Place buildings ¶
¶
First
~~~~ ¶
local hq = plr:place_building("empire_headquarters", plr_sf, false, true) ¶
~~~~ ¶
¶
¶
The above function returns
¶
__Importand remark about local:__ Whenever you assign variables with values, you should do it this way: `local name_of_variable = value`. 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
¶
Consider to have an object like a car. This car has a property: color. But this car has also some functions: drive_straight(), drive_left(), drive_right(). If you want to get the color, you want to access the property: `clr = car.color.` 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: ¶
¶
* Properties (can be accessed through a point): [Player.name](/docs/wl/autogen_wl_game/#wl.game.Player.name), [Map.width](/docs/wl/autogen_wl_map/#wl.map.Map.width) ¶
* Functions (must be called with a colon): [Player:sees_field(f)](/docs/wl/autogen_wl_game/#wl.game.Player.sees_field), [Map:get_Field(x,y)](/docs/wl/autogen_wl_map/#wl.map.Map.get_field) ¶
¶
Now its time for a first test. Run: ¶
¶
~~~~ ¶
./widelands --scenario=/full/path/to/the/map.wmf ¶
~~~~ ¶
¶
You should see the placed headquarters 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()](/docs/wl/autogen_wl_bases/#wl.bases.PlayerBase.place_building) returns
~~~~ ¶
hq:set_wares("log",10) ¶
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 to set_wares(): ¶
~~~~ ¶
hq:set_wares({ ¶
log = 10, ¶
planks = 20, ¶
granite = 20, ¶
marble = 5, ¶
}) ¶
~~~~ ¶
¶
Same goes for setting workers and soldiers: ¶
~~~~ ¶
hq:set_workers({ ¶
empire_builder = 5, ¶
empire_carpenter = 2, ¶
empire_lumberjack = 5, ¶
}) ¶
¶
hq:set_soldiers({0,0,0,0}, 45) ¶
~~~~ ¶
¶
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/'_. Add as many wares and workers as you like. After your'e done you should make a test run again. ¶
¶
Now its time to create
¶
## main_thread.lua and texts.lua ¶
¶
The file _'main_thread.lua'_ contains
¶
### Message boxes ¶
¶
You may have seen that our scenarios have some message boxes to inform the player about the story. In
~~~~ ¶
campaign_message_box({title = "This is the title", body = p("Hi, this is my first scenario")}) ¶
~~~~ ¶
¶
¶
The file _'main_thread.lua'_ must be loaded, so add an _include_ as last line in _'init.lua'_: ¶
~~~~ ¶
-- Load own lua scripts ¶
include "map:scripting/starting_conditions.lua" ¶
include "map:scripting/main_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 in _'main_thread.lua'_ it is
¶
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"), ¶
} ¶
~~~~ ¶
¶
File _'main_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 values 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()](/docs/wl/autogen_wl_game/#wl.game.Player.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 ¶
¶
[Solution for playing with message box attributes](#play-with-message-box-attributes) ¶
¶
### Triggering events - adding logic ¶
¶
¶
* 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
¶
* Checking the return value of the function [Player:sees_field(field)](https://wl.widelands.org/docs/wl/autogen_wl_game/#wl.game.Player.sees_field) ¶
* Checking the property [Field.owner](https://wl.widelands.org/docs/wl/autogen_wl_map/#wl.map.Field.owner) ¶
¶
Both possibilities needs a field to work with. Fields are part of the map and can be accessed through `wl.Game().map`.
~~~~ ¶
-- 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
¶
~~~~ ¶
-- Defining a function with a loop ¶
function check_field_owner() ¶
local field = map:get_field(10,0) ¶
while true do -- Start defining a loop ¶
print("Field: ", field , "Owner: ", field.owner) ¶
sleep(1000) -- An important row, see below ¶
end -- End of defining the loop ¶
end -- End of defining the function ¶
¶
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 you 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 is will be executed also. In this case: The message box is shown. So it is possible to start more 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_! ¶
¶
The loop created above will never end.
~~~~ ¶
function check_field_owner() ¶
local field = map:get_field(10,0) ¶
while field.owner ~= plr do -- Run loop as long 'field.owner NOT EQUAL plr' ¶
sleep(1000) ¶
end ¶
campaign_message_box( {field = field, title = "Field owned", body = p("This field is now owned by me")} ) ¶
end ¶
¶
run(check_field_owner) ¶
~~~~ ¶
¶
The loop runs now until 'field.owner __not equal__ plr'. Such a comparison returns either 'true' or 'false'. See also [Rational Operators](https://www.lua.org/pil/3.2.html) for other types. For other types of loops see [Control structures](https://www.lua.org/pil/4.3.html). ¶
¶
As soon the message box appears,
¶
Exercise: ¶
¶
* Move the table of the message box into the file _'texts.lua'_ and make the corresponding changes ¶
* Add a new function plr_sees_field() and use [Player:sees_field()](/docs/wl/autogen_wl_game/#wl.game.Player.sees_field) for the condition. You have to choose another field though, one which is outside the area the player sees, for testing purposes i have used 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 right, you should see at least two message boxes popping up: One after the first loop has ended, and the new one after the second loop has ended. ¶
¶
If you can't get it to work, [here is the Solution](#add-another-function). ¶
¶
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](#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 _'main_thread.lua'_ whereas put texts in other files ¶
* Include each other file either from _'init.lua'_ or at the beginning of _'main_thread.lua'_ ¶
* Use always the keyword `local` when defining variables ¶
* Add events as functions and execute them through `run(function)` ¶
* Don't forget to use `sleep()` in loops ¶
* To verify variables you can always add a `print()` statement ¶
* Changing logic of coroutines have only affect if you start a new game ¶
¶
## objectives.lua ¶
¶
If you have played some of our scenarios you have seen that there are objectives a player has to fulfill. Let's add an objective and also some other stuff. At the end the scenario should do the following: ¶
¶
1. If the field is seen by the player, scroll
2. Show a message box to the player ¶
3. If the player clicks ok in the message box, scroll back to the previous location ¶
4. Add an objective saying that he should build a forester, and a woodcutter ¶
5. Show a message box if the objective is done ¶
¶
### Smooth scrolling of the map ¶
¶
~~~~ ¶
function check_field_owner() ¶
local field = map:get_field(10,0) ¶
while field.owner ~= plr do ¶
sleep(1000) ¶
end ¶
local cur_view = scroll_to_field(field) -- Call the function and store the return value ¶
campaign_message_box(field_reached) ¶
scroll_to_map_pixel(cur_view) -- Use the return value to scroll back to 'cur_view' ¶
end ¶
¶
run(check_field_owner) ¶
~~~~ ¶
¶
### Check the amount of build buildings ¶
¶
The related function to get wares in stock is: [Player:get_wares()](https://wl.widelands.org/docs/wl/autogen_wl_bases/#wl.bases.PlayerBase.get_wares). But for now we implement the automatic scrolling. The used functions are part of [ui.lua](https://wl.widelands.org/docs/wl/autogen_auxiliary_ui/#ui-lua) which we have [included in ini.lua](#including-some-helpers) and are called _scroll_to_field()_ and _scroll_to_map_pixel()_. ¶
¶
¶
## Solutions ¶
¶
### Play with message box attributes ¶
¶
~~~~ ¶
-- Store the table in the variable 'first_message' ¶
first_message = { ¶
title = "This is the title", ¶
body = p("Hi, this is my first scenario"), ¶
posx = 1, ¶
posy = 1, ¶
w = 200, ¶
h = 200, ¶
} ¶
~~~~ ¶
¶
### Add another function ¶
¶
Does your solution look like this one? ¶
~~~~ ¶
-- Added function to 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(plr_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 ¶
~~~~ ¶
¶
## Tips for testing coroutines ¶
¶
* scriptconsole F6 ¶
* dofile("filename") ¶
* commenting coroutines ¶