Wurst not installed yet? Follow the Setup Guide and create a new Project.
This guide expects the reader to have basic knowledge of functions and variables.
Hello and welcome to WurstScript. This guide is an informal introduction to the Wurst workflow and an easy way to verify your installation. We will cover how to structure code, use the standard library, create data objects through code and run the final product in warcraft 3. This guide will not explain the core principles of programming, i.e. how functions and variables work and how we use them.
If you setup your project correctly using the setup app, there will be many folders and files generated in the project folder you chose. Otherwise setup your project now. Load the project in VSCode by opening the project folder (File
-> Open Folder...
). It is important to open the project folder so that Wurst can detect the project. After opening you should see something like this:
Let’s go through all the files:
Make sure to always open a .wurst file. Otherwise the vscode plugin might not load.
Open the Hello.wurst file inside the wurst folder to start the wurst plugin. Run the project by opening the command console F1
and using the >runmap
command.
The text Hello World will be displayed.
Let’s take a look at the code:
package Hello
/* Hello and welcome to Wurst!
This is just a demo package.
You can modify it to tests things out
and then later delete it and create your own, new packages.
Enjoy WurstScript? Consider starring our repo https://github.com/wurstscript/WurstScript */
init
print("Hello World")
The first line
package Hello
is the package declaration. Each file contains exactly one package that is named like the file without the ending. Apart from comments this must be the first line in any .wurst file. The next bunch of lines
/* Hello and welcome to Wurst!
This is just a demo package.
You can modify it to tests things out
and then later delete it and create your own, new packages.
Enjoy WurstScript? Consider starring our repo https://github.com/wurstscript/WurstScript */
are comments. The final two lines
init
print("Hello World")
consist of an init-block by just containing the init keyword without indentation. And then everything indented after this keyword is inside it’s block of contents, such as the print statement, which displays the given text on the screen for all players. All blocks automatically end at the end of the file, such as the init and package blocks in this example.
Let’s go into further detail about what we just did.
If you have used (v)Jass before, you might wonder where the print came from in use above.
It is neither a hardcoded wrapper around BJDebugMsg()
, nor internal to the wurst language. Instead it simply is a function defined in a package inside the standard library (aka stdlib/stl).
WurstScript ships with a standard library that contains a vast collection of convenient wrappers, utility packages, and powerful systems, which help in the creation of any map, let you focus on content and aid in maintaining a consistent syntax for high-level programming.
The standard library documentation can be found HERE
It has been automatically imported when you created your project using the setup tool and is now inside the _build/dependencies/wurststdlib2
folder in your project’s root. Since this is an external dependency, you should not modify these files. However, feel free to look at and learn from them. To update your stdanrd library dependency, use the setup’s Import
functionality.
You can visit the Printing
package in vscode by holding down the ctrl key and then clicking the function call, i.e. anywhere on the word print.
The functions inside the Printing
package then reference the original warcraft natives found in common.j and blizzard.j.
You can use the global vscode search or file search (ctrl+p
) to find packages you’re looking for.
In Wurst, it is generally not recommended to call most natives directly. Instead, an extension function should be used to make the code more concise, readable, consistent and documented. Extension functions can also be easier found via autocomplete.
Let’s look at some examples.
Jass:
local integer id = GetPlayerId(GetOwningPlayer(GetTriggerUnit()))
In the Jass variant, even though the code is read from left to right, the order of execution is actually inverse, GetTriggerUnit()
being the first expression to be evaluated.
Contrast that with Wurst:
let id = GetTriggerUnit().getOwner().getId()
In the wurst counterpart the code is executed in the same order it is read. Implicit meanings are omitted and there are no nested brackets. Users of Java and other higher level programming languages will find this as no surprise, and indeed new users too will probably agree this way of coding feels more natural. This functionality arises from another automatic import from the standard library - the Unit
API.
For a complete list of all coding conventions, visit the manual.
Jass:
local unit u = GetTriggerUnit()
call SetUnitX(u, 1000.)
call SetUnitY(u, 2000.)
call UnitAddAbility(u, 'hble')
call SetUnitPaused(u, true)
Jass doesn’t support cascading/chaining functions on a mutual target. You have to resort to local variables or repeated native calls.
Wurst support chaining via the cascade operator:
GetTriggerUnit()..setX(1000.)
..setY(2000.)
..addAbility('hble')
..setPaused(true)
Extension functions not only wrap existing natives, but also provide convenience functions. An improved version of the snippet above would be:
GetTriggerUnit()..setXY(1000., 2000.)
..addAbility('hble')
..pause()
Vectors are a very useful construct for positional calculations in 3d space. However, they add a lot of overhead if implemented as a class, which is why even vJass doesn’t use them in any common libraries. Warcraft provides the location
type - however, it is painful to use, and permanently leaks.
Wurst implements vectors via a tuple-type, which don’t need to be allocated and deallocated like classes/structs, but can still provide an elegant API.
vJass example:
struct PreviousPointTracker
real x = 0.
real y = 0.
real angleToLast = 0.
method add(real addX, real addY) returns nothing
local real oldX = x
local real oldY = y
set x = x + addX
set y = y + addY
set angleToLast = Atan2(y - oldX, x - oldY)
endmethod
endstruct
In the wurst pendant below, you can see that the vector types provide overridden operators to use with vector maths.
class PreviousPointTracker
var pos = vec2(0., 0.)
var angleToLast = angle(0.)
function add(vec2 addedVec)
let oldPos = pos
pos += addedVec
angleToLast = pos.angleTo(oldPos)
Note that we used the type angle
instead of a plain real. This is also a tuple-type and therefore as efficient as a normal real, but provides domain specific methods for working with angles. It also avoids some common mistakes like confusing angles in radians and degrees.
Think of ExampleMap.w3x
as a “terrain” map. It’s the place you go to make terrain, doodad, and destructable changes; and wurst uses that as a starting point when compiling a mapfile for test or release. Other changes like gameplay constants also go in the terrain map, but the point remains the same.
The terrain map is used read-only by wurst, meaning that wurst never edits or saves over that file. This is nice because it means you can edit the terrain in the world editor and the code in vscode at the same time.
When you execute the buildmap
task in vscode, the output mapfile - based on the terrain and wurst.build
file, including all of your compiled code - is generated into the /_build
folder.
It’s this which you can use to release and distribute your map online.
The runmap
command functions similar, however the map is named WurstRunMap.w3x, gets copied into your warcraft maps folder and Warcraft III gets executed with the appropriate arguments to run the map. Use this command to test your map.
One other cool feature is that wurst will automatically import contents of the imports/
directory into the built mapfile. This can be convenient for quickly adding a model file for use in a spell, for example. The folder paths inside the imports folder will be retained.
If you want to use the imported resources inside the terrain map, copy the built mapfile from the /_build
folder over to the project’s root. Open it with the World Editor, and save it, clearing wurst’s generated code. Now you’ll have a terrain map with imports, which is also suitable as a starting point for the wurst editor.
As you can see, wurst has a powerful API for the types and functions exposed by jass to write code at a slightly higher level. As a beginner, one of the your first questions will be about how to find the right library code to do what you want without resorting to jass natives. Three quick tips on that:
u
and you want to kill it, see what vscode can offer you by typing u.k
followed by ctrl-space
.If you’re looking for an equivalent to some vJass library found on hiveworkshop, your approach might differ a bit.
Table
, wurst users are encouraged to use the generic HashMap<K, V>
which makes much nicer and more type-safe code.We created an example spell in march’s blog post. You should check it out!
Let’s take a closer look at the code.
package Conflagration
import ConflagrationObjects
import ClosureTimers
import ClosureForGroups
import HashMap
import ClosureEvents
let buffId = BUFF_OBJ.abilId
let buffMap = new HashMap<unit, CallbackSingle>()
init
EventListener.onPointCast(SPELL_ID) (caster, tpos) ->
flashEffect(SPELL_EFFECT_PATH, tpos)
doAfter(SPELL_EFFECT_DURATION) ->
forUnitsInRange(tpos, SPELL_RADIUS) u ->
if u.hasAbility(buffId)
caster.damageTarget(u, BONUS_DAMAGE)
flashEffect(BONUS_EFFECT_PATH, tpos)
caster.damageTarget(u, BASE_DAMAGE)
u.addAbility(buffId)
if buffMap.has(u)
destroy buffMap.get(u)
let cb = doAfter(BUFF_DURATION) ->
if buffMap.has(u)
buffMap.remove(u)
u.removeAbility(buffId)
buffMap.put(u, cb)
We import various packages to make the process of spell creation easier. ClosureTimers
and ClosureForGroups
allow us to easily handle delayed code (doAfter
) and group enums(forUnitsInRange
). ClosureEvents
gives us a nicer API to wc3 events, including closures. At last HashMap
allows us to attach arbitrary data to other data - in this example we attach our Buff effect to the target unit.
The spell works like this: We listen to a specific spell event using EventListener.onPointCast
. The lambda block contains our event callback. Inside the callback we create the meteor effect using flashEffect
at the appropriate positions. Since the effect takes some time to hit the ground, we also want our damage effect to be delayed. Thus we start a timer using doAfter(SPELL_EFFECT_DURATION) ->
. In this case the lambda block is executed once the chosen amount of time has passed. Now that the special effect has landed on the ground, we apply damage to all units inside a circular area around the target position. We use forUnitsInRange(tpos, SPELL_RADIUS) u ->
to have all units inside the range near the target possition passed to the lambda function. This is the u
in front of the arrow ->
in SPELL_RADIUS) u ->
. Inside the lambda block we now refer to u as one of the units that is in range, and apply our effects.
In case the unit already has the affect, we apply some extra damage and flash an effect in the following if-statement. After that we apply the normal spell damage and add the dummybuff ability to the target (if the unit already has the ability it just has no effect). The next if branch checks whether the buffmap already has an entry for our unit - if that’s the case, we destroy the existing timer to end the buff. Then we start the new timer to end the buff when it exceeded it’s duration.
let buffMap = new HashMap<unit, CallbackSingle>()
Here we create the buffmap we talked about. Basically it allows us to save one closures instance from ClosureTimers
per unit, that refers to a running timer for a buffs duration.
We hope this guide helped you to get started right away with developing your map on wurst. If you have further questions or would like to hang out with wurst people, drop by our Discord.