MakingCampaignInWML4

From The Battle for Wesnoth Wiki
Revision as of 16:53, 25 November 2021 by Jarom (talk | contribs) (Created page with "'''Custom macros and events''' After going through part 1, part 2 and part 3 we know the basics of m...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Custom macros and events

After going through part 1, part 2 and part 3 we know the basics of making a campaign - you could almost make one with the information you have now, but let's be honest, that would be kinda boring. We're going to improve the storytelling and gameplay using events, but before that we're going to learn how to make our own macros because that's extremely useful for making various things.

Creating custom macros

Up until now we've been using macros defined by the game and if you checked game data out of curiosity, you might know how they are made. It's good to know how to find them, but that's not necessary for now. We can place our macros anywhere, but for those that are used in various scenarios we typically place them in utils or macros folders - practice varies, as both names are not recognized by [binary_path]. Therefore, we're going to first add the folder to our campaign. To do that, in _main.cfg, in #ifdef section, we're going to place the following line:

    {~add-ons/WML_Tutorial/utils}

It tells the game to load all .cfg files inside the utils folder of our add-on, just like we did with scenarios.

Macros must be defined before they are used, so place this before all other similar lines, notably the one loading scenarios, but not necessarily before [binary_path] because it's typically not using it.

Obviously you have to adjust the content of the line to match your add-on's folder name, and if you're using macros instead of utils as folder name, change that too, but hopefully that's something you know at this point.

Now that we have included the folder, we have to create a file with .cfg extension inside. If it's a generic file, we typically name it utils.cfg or macros.cfg, if it's something more specific we can name it scenario-macros.cfg, bigmap.cfg, abilities.cfg etc. For the purpose of this tutorial we're going to create one named heroes.cfg, and the other one named bigmap.cfg.

Making ("defining") a custom macro is typically like that:

#define MACRO_NAME ARGUMENT1 ARGUMENT2
    [tag]
        key = value
        other_key = its_value
    [/tag]
#enddef

What's important is the #define MACRO_NAME, the argument list in the same line and the #enddef at the end, but it's best to look at examples if in doubt.

Creating reusable hero macros

We have just one scenario for now, but typical campaigns have more. We have two characters from the beginning, but it's common that some will join us later on. When testing the campaign using :debug and :cl/:n commands it's common that we don't get all of them, which might break our recall logic.

For that reason Wesnoth offers a special create/recall syntax: if we try to create a new unit with the same id as one of units on the recall list, the unit from the recall list will be recalled in the specified place instead, and if not, the create action will proceed as normal.

Now we don't want to copy our heroes' code in every place they might appear, because if we're going to change anything, we'd have to change it everywhere. So we're going to move just the contents of their [leader]/[unit] tag into separate argumentless macros. Why only contents? Because it will make it easier to add some situation-specific keys to the tag this way, and use either [leader] or [unit] tags depending on circumstances.

TL;DR: We're moving some of the code to a macro shared between scenarios because it might be helpful later.

Let's start with Lezarek. Add a code like that to heroes.cfg:

#define HERO_LEZAREK
    id = Lezarek
    name = _"Lezarek"
    unrenamable = yes
    type = Longbowman
#enddef

And in the scenario we leave:

        [leader]
            {HERO_LEZAREK}
        [/leader]

It's customary to prefix macros with something meaningful (HERO_ in this case), it makes it easier for autocomplete tools to help us and whoever is going to read this code later (typically you) will have it easier to guess what it does. It also helps in preventing collisions in macro names.

Now with Verbott we're not going to move everything. To heroes.cfg we'll append:

#define HERO_VERBOTT
    id = Verbott
    name = _"Verbott"
    unrenamable = yes
    type = White Mage
    gender = female
    [modifications]
        {TRAIT_LOYAL_HERO}
        {TRAIT_INTELLIGENT}
    [/modifications]
#enddef

And in the scenario we leave:

        [unit]
            {HERO_VERBOTT}
            placement = leader
        [/unit]

Because placement is something that might change between scenarios.

And that's it. You should be able to reuse your heroes in future scenarios now.

Creating and tracking a map

It was mentioned in the previous part that you can add your map to the intro, and now we're going to learn how. First, in the bigmap.cfg file we created before, add a macro like that:

#define TUTORIAL_MAP STAGE
    [story]
        [part]
            [background_layer]
                image = maps/background.jpg
                scale_horizontally = no
            [/background_layer]
            [background_layer]
                image = maps/wesnoth.png
                scale_horizontally = no
                base_layer = yes
            [/background_layer]
            show_title = yes
            {STAGE}
        [/part]
    [/story]
#enddef

Now this is a big piece of magic code and you're not going to modify most of it, to the point that you might question why it's not a default wesnoth macro. You can change the name of the macro of course. If you have your own map, replace just the image = maps/wesnoth.png line, but it's unlikely that you're making your own background, so you probably won't replace the other layer.

Further explanations are for the curious ones:

Most of the keys in [background_layer], that is, scale_vertically=, scale_horizontally= and keep_aspect_ratio= are there to make the graphics scale properly and fill the screen of any device without looking weird. However, default values of scale_vertically (yes) and keep_aspect_ratio (yes) are good for us, so we don't have to set those manually.
Meanwhile, base_layer = yes makes this picture the one, to which all coordinates of subsequent [image] tags (usually hidden in macros) are aligned to, and make them scale properly along with the map picture. All of those are well-thought means to keep the map looking nice on any screen size, and have to be modified only if you wish to achieve some clever non-standard effects, but that's unlikely.

You might notice that we need an argument for this macro. This argument is typically another macro, whose contents are macros describing images… yeah, that's a lot of macros in macros. Let's make those for scenario 1:

#define JOURNEY_01_NEW
    {NEW_BATTLE 191 570}
#enddef

This will place a single battle marker somewhere northeast of Blackwater port when given as argument to the macro before. Now all we need to do is place the following macro call in scenario, preferably just after [story] to keep it logically together:

    {TUTORIAL_MAP {JOURNEY_01_NEW}}

But that's for the first scenario. To make one for the second scenario we typically do something like that:

#define JOURNEY_01_OLD
    {OLD_BATTLE 191 570}
#enddef

#define JOURNEY_02_NEW
    {JOURNEY_01_OLD}
    {NEW_JOURNEY 204 564}
    {NEW_JOURNEY 211 557}
    {NEW_JOURNEY 219 550}
    {NEW_BATTLE 230 537}
#enddef

Note that we first created a macro old journey. You could ask why, when all it does is to provide one value for the next journey - well, there are two reasons, first, it helps maintain some logic, as the OLD macro is practically the same as the NEW macro, and second, if we wanted to add third scenario, we'd start with making something like that:

#define JOURNEY_02_OLD
    {JOURNEY_01_OLD}
    {OLD_JOURNEY 204 564}
    {OLD_JOURNEY 211 557}
    {OLD_JOURNEY 219 550}
    {OLD_BATTLE 230 537}
#enddef

This time it's much more than just one value, and it reuses the previously created OLD macro. We can chain OLD macros as much as we want and making changes in journey in one stage will automatically propagate to all subsequent journeys without having to copy the change manually.

For selecting a proper positioning for journey markers, you mostly have to experiment yourself. Things you might want to know if you plan to do that:

  • Markers are centered on the point you select
  • You can use NEW_REST/OLD_REST instead of NEW_BATTLE/OLD_BATTLE for dialogue and cutscene scenarios, it looks like a flag instead of crossed swords - see HttT
  • Battle and rest icons are bigger than journey dots, so you typically want to give them slightly more space around
  • It's best to first make the NEW journey, often starting with placing the rest/battle marker and then making a believable route to them, then experiment with it a lot to make it smooth and good looking, and only after that copy the content exactly to OLD, and run the search/replace function from NEW to OLD on this part - you'll save some editing time that way

Creating a macros only used in one file and inline macros

Sometimes you're going to frequently reuse a part of a code in one scenario but not in other places. There's no point in placing it inside utils/macros then, as it's often meaningless without context. So you should define it somewhere in that file, and then you can use #undef directive to get rid of it afterwards. Don't use #undef with macros in utils, even if they are not used anymore after some scenarios - this might cause weird errors. A not very rare usage of macros is placing some text in the middle of a line and it's important that the macro itself doesn't cause a line break. If you're attempting something like that, place #enddef in the same line as the content. We're going to use both to optimize our _main.cfg file for possible future changes. First, add the following lines, preferably after #textdomain but definitely before [textdomain]:

#define WML_TUTORIAL_PATH
data/add-ons/WML_Tutorial#enddef

Of course, you should replace WML_Tutorial with your folder name if it's different, and the macro name as well.

As you can see, we placed here a part of text that's typically used at least twice in your _main.cfg file (more if you use custom images). Now we can replace our path line inside [textdomain], that originally looks like:

    path = data/add-ons/WML_Tutorial/translations

with

    path = {WML_TUTORIAL_PATH}/translations

And similarly the path in [binary_path]:

        path = {WML_TUTORIAL_PATH}

If you used custom images in [campaign], you should change their lines as well.

Finally, at the last line you can put the following:

#undef WML_TUTORIAL_PATH

to get rid of the macro after it's not needed anymore (we'll typically be using either ~add-ons/ path or ones relative to [binary_path] in other files). This helps with keeping the macro list clean and potentially avoid conflicts.

You can say that character count-wise that's not a good difference, but note that your macro name can be shorter, and if you change your folder name for some reason you only have to change it once. Well, for just two usages you might not really need it, but it serves as a good example. Make sure your campaign works after those changes, as mistakes in this part can easily break it.

Events, actions and objectives

Probably the most common thing in Wesnoth are events. You'll surely encounter them during campaign/scenario development, and sometimes during units/multiplayer development as well. In their simplest form they can be expressed as: When a certain thing happens, do some actions.

We won't be covering all details of event development here, but we'll go through some common use cases and you should be able to figure the rest by yourself to a certain extent. You can check the list of in-game events you can use in EventWML and all the WML action tags that can be used in response in ActionWML.

Let's start with a very common filterless events: prestart and start. The former is typically used for advanced scenario setup - advanced versions of [unit], [item] and [label], setting up variables etc. The latter is usually used for initial dialogues and objectives.

We already wrote a prestart event for our scenario without much explanation and we're going to leave it unchanged for now, keep in mind that you can have more than one prestart event and they will all run one after another before the game screen is shown to the player.

Writing a starting dialogue

We're going to write a start event now. Without any actions it'll simply look like that:

    [event]
        name = start 
    [/event]

But the essence of events are actions. In this case we're going to use the [message] tag, a part of InterfaceActionsWML. It's very flexible, but most of the time you're going to use its basic form with only speaker= and message= keys, and speaker= is the id of the unit that should speak the content of the message= key.

We'll put the following inside [event]:

        [message]
            speaker = Lezarek
            message = _"A necromancer! To arms, my friends!"
        [/message]
        [message]
            speaker = MalShack
            message = _"Rejoice, puny villagers! Tonight you'll join the ranks of eternals!"
        [/message]

Yeah, typical SotA style necromancer attack. By the way, out of 16 mainline campaigns as of 1.16.0, 6 start with orc/goblin attack, 3 with undead attack and 2 with bandit attack. Judge for yourself whether it's good or not to increase that number, but it's seemingly nothing shameful to use that cliche.

The dialogue writing is up to you, but it's strongly preferred to have an English version in the code for various important reasons, and use translations to make your own language's version, but it's not an absolute necessity.

Note that we usually place events after [side] tags to keep some logical order in our file, but it's not a strict rule.

Writing objectives: basics

Let's also add some custom objectives at the end of our start event. You probably noticed while testing that there's a default objective - defeat enemy leaders. Writing any custom ones will replace it, and new objectives replace old objectives.

Writing objectives is important and somewhat complex. We'll start with the basics, that is, a custom win condition. Keep in mind that it's just a description, it doesn't do anything by itself, but the example will make use of the default win condition which is: all enemies defeated (we only have one enemy).

        [objectives]
            [objective]
                description = _"Defeat the necromancer Mal-Shack"
                condition = win
            [/objective]
        [/objectives]

The [objectives] tag groups particular objectives into one screen, and a new [objectives] tag will replace the old one. If you don't want to change anything, use [show_objectives] instead. Inside [objectives] you should use [objective] tag. The ones with condition = win are the player's goals, and will be shown before those with condition = lose that describe the player's defeat possibilities, so you should also sort them like that in the code to avoid confusion.

We should also add a defeat condition. The easiest one is the turn limit. All you need to do is to place the macro {TURNS_RUN_OUT} inside [objectives] and you'll get a standardized (and usually translated) one. Opposite macro is {HAS_NO_TURN_LIMIT} but it's often obvious and omitted. There are some other macros with standard texts in objective-utils.cfg macro file.

A hero's death

We'll obviously lose if our leader dies, but we won't if a hero dies, and both are not written in objectives yet. It's a well-known trick to move both death events and objectives to a separate file in utils or macros folder - typically heroes file or deaths file. In this case we're going to use the heroes.cfg file.

First, let's write an objective for each. You can also refer to the sota-utils file, because there are good solutions there.

#define OBJECTIVE_DEATH_LEZAREK
    [objective]
        description = _"Death of Lezarek"
        condition = lose
    [/objective]
#enddef

#define OBJECTIVE_DEATH_VERBOTT
    [objective]
        description = _"Death of Verbott"
        condition = lose
    [/objective]
#enddef

Now if you're going to have them together for the rest of the campaign, consider creating just one macro for both, e.g. OBJECTIVES_DEATHS, but if they are typically together but not always, you can make one macro combining the previous macros:

#define BASIC_DEATH_OBJECTIVES
    {OBJECTIVE_DEATH_LEZAREK}
    {OBJECTIVE_DEATH_VERBOTT}
#enddef

And we're going to put {BASIC_DEATH_OBJECTIVES} into our scenario's [objectives] tag.

Now we actually should handle their deaths:

#define HERO_DEATH_EVENTS
    [event]
        name = last breath
        [filter]
            id = Verbott
        [/filter]
        [message]
            speaker = Verbott
            message =  _"No! How could the great me meet my end in this nowhere!"
        [/message]
        [endlevel]
            result = defeat
        [/endlevel]
    [/event]
    [event]
        name = die
        [filter]
            id = Lezarek
        [/filter]
        [message]
            speaker = Verbott
            message =  _"Lezarek! How dare you die on me! I can't lead troops by myself!"
        [/message]
        [endlevel]
            result = defeat
        [/endlevel]
    [/event]
#enddef

Note the [filter] tag. Both last breath and die events must have a filter tag that will narrow down units whose deaths will trigger those events - we don't bother with deaths of simple cannon fodders. This tag is very widespread in various events and actions, so there's no running from it. It usually takes a StandardUnitFilter as content, but there are also other filters for different tags, see FilterWML. Obviously there's no filter for start and prestart events.

The [endlevel] tag from DirectActionsWML will cause the scenario to end, either as victory or defeat depending on the value of result= key.

The second event is actually not strictly necessary in most cases, because we'd lose anyway with no leader, but it's customary to at least say some last words, or if there are more important characters, to say farewell to your dead ally. You can notice that we used different events for both of them - last breath is when the unit is about to die, but it's still there with 0HP or less, while die is after it falls into the abyss of nothingness so it can't speak anymore. Sometimes you want to split your death events into two - last breath with the unit's last words, and die with [endlevel] after visual effects of dying.

Don't forget to put our {HERO_DEATH_EVENTS} macro call somewhere in the [scenario] (often in the end).

Alternative win condition: move to...

You might notice that in some scenarios your goal is to move your leader to a specific place rather than killing enemies. We'll go through the basics of this approach. First, an [objective]:

            [objective]
                {ALTERNATIVE_OBJECTIVE_CAPTION}
                description = _"Move Lezarek to signpost in the north"
                condition = win
            [/objective]

It goes to our [objectives] of course. You should only use {ALTERNATIVE_OBJECTIVE_CAPTION} when completing either objective results in win. Sometimes you need to complete both objectives, or one of them is a side/bonus objective.

Now let's write the event that will handle this objective:

    [event]
        name = moveto
        [filter]
            id = Lezarek
            x,y = 3,1
        [/filter]
        [message]
            speaker = Lezarek
            message = _"We abandon the village! Follow me!"
        [/message]
        [endlevel]
            result = victory
        [/endlevel]
    [/event]

Moveto events are typically used to handle a situation where a certain unit moved to a specific place. You can define the location in the [filter_location] subtag of [filter], but often you just need x and y coordinates that can be given directly in [filter] and you should treat it as: there exists an unit matching the filter that just finished moving.

Note the x,y = 3,1 line - it's a shorthand for two lines, x = 3 and y = 1. You can use it with most arbitrary keys that only have one value, but in cases other than x,y it usually impacts readability.

Alternative win condition: survive

Another common win condition is to survive a certain number of turns. Currently we lose if turns run out, but we can change that. First, let's remove the {TURNS_RUN_OUT} objective and replace it with:

            [objective]
                {ALTERNATIVE_OBJECTIVE_CAPTION}
                description = _ "Survive until turns end"
                condition = win
                show_turn_counter = yes
            [/objective]

And now handling it:

    [event]
        name = time over
        [message]
            speaker = Lezarek
            message = _"The reinforcements are about to come! Surrender, you evil necromancer!"
        [/message]
        [message]
            speaker = MalShack
            message = _"Kuh! Persistent ones... I'll be back!"
        [/message]
        [endlevel]
            result = victory
        [/endlevel]
    [/event]

And that's it - our victory [endlevel] will be called before automatic defeat from time limit. Note the lack of [filter].

Keep in mind that typically scenarios have only one win condition, occasionally complex one.

Variables and conditionals

You can save some things, usually game state, for later use. If you have some experience with programming, you know why it's important, and if you don't, well, it might come in handy.

Let's assume the following scenario - the keep of our enemy's side has something important for some reason that should be better explained in dialogues, and we should get a bonus in gold when we win if we manage to snatch it.

First, let's declare a variable in our prestart event. You can do that using [set_variable] from InternalActionsWML, but typically you're going to use {VARIABLE} macro like this:

        {VARIABLE enemy_keep_visited no}

The first argument is the name of the variable to set and the second argument is the desired value. Now we should add a separate moveto event that will handle visiting the enemy keep:

    [event]
        name = moveto
        [filter]
            side = 1
            [filter_location]
                location_id = 2
            [/filter_location]
        [/filter]
        [message]
            speaker = unit
            message = _"I've recovered the item!"
        [/message]
        {VARIABLE enemy_keep_visited yes}
    [/event]

It's slightly different from the moveto event we know. First, we don't point to a specific id - any unit of side 1 (in this case player's) will trigger it. Second, we don't specify x and y, but location_id instead - we did the same when placing a label in prestart event, even in the same location - regardless of wherever this location actually is on the map. Convenient.

Second, we used speaker = unit - obviously there isn't any unit with id like that, it's a special value that points to the unit that acted in the event (if there is any), in this case the unit that moved to the enemy keep. It's actually a variable as well, and in some events there's an analogical second_unit, for example in last breath and die events it's the unit who killed the unit in the [filter].

The {VARIABLE} macro is the same as last time, just with a different value. It will overwrite the previous value.

Now we need to handle that gold giving stuff. We'll do that using a victory event to make sure we'll get it regardless of how we won.

    [event]
        name = victory
        [if]
            [variable]
                name = enemy_keep_visited
                boolean_equals = yes
            [/variable]
            [then]
                [gold]
                    side = 1
                    amount = 100
                [/gold]
            [/then]
            [else]
                [message]
                    speaker = Verbott
                    message = _"It's unfortunate that we didn't manage to obtain that item."
                [/message]
            [/else]
        [/if]
        {CLEAR_VARIABLE enemy_keep_visited}
    [/event]

The [if]-[then]/[else] is a typical conditional structure that can be used in many different places. This, along with [variable] and other condition tags can be found in ConditionalActionsWML, so make sure to read it someday - I'm not going to elaborate on those.

The [gold] tag from DirectActionsWML gives the specified amount of gold to the given side. That's it.

The {CLEAR_VARIABLE} macro is a shorthand for [clear_variable] tag, which is used to get rid of a variable completely after it's no longer useful - otherwise it will persist between scenarios and clutter the savegames. It'll silently do nothing if the variable was already cleared before or never existed.

It's also possible to pass a variable's value to any key or even a string, using the so-called variable substitution like that:

[message]
    speaker = $unit.id
    message = _"I'm $unit.name!"
[/message]

It takes advantage of the previously mentioned unit variable specific to some events with filters, like moveto or die.

See VariablesWML/How to use variables for a more in-depth tutorial on variables.

Next scenario

When you create the second scenario just like you created the first one, you probably want to be able to move from scenario 1 to scenario 2. There are 2 ways to do that - first, you can add a next_scenario = id_of_second_scenario key to your [scenario] tag. This will provide a default for most cases, including the :n debug command. Second, you can add exactly the same key to a specific [endlevel] tag - this way allows you to branch your scenario progression as you wish, just like with, for example, Muff-Malal Peninsula/Isle of the Damned in HttT. You can even pass a value of a variable containing the next scenario's id into the next_scenario= key if you have really complicated progression logic. But usually you're just going to put it directly in [scenario] and be done with it.

Other features

In this tutorial we barely scratched the surface of deep possibilities of WML, especially ActionWML, but we covered most of the common usages for campaigns and you can always read the actual documentation to find what you need.

This page was last edited on 25 November 2021, at 16:53.