- Joined
- Jan 1, 2009
- Messages
- 1,615
WurstScript
by peq & Frottywith contributions from Crigges & muzzel
WurstScript is a Scripting language, which can, similar to vJass, cJass and zinc, be translated to Jass.
Why a new Scripting language?
The only alternative to Wurst is vJass (or cJass, zinc, ..., which are basically all the same).
If you are interested in what we did not like about vJass click the button:
1. Editor Support
vJass is mostly based on text-replacements and generation (vJass modules, textmacros). This makes it hard to create a good editor for vJass, including features like autocomplete.
Wurst instead goes for simple, structured code, that can be analyzed in a single compiler-phase. vJass however needs more phases.
2. Type-safety
vJass isn't typesafe. Look at this example:
*vJass Code*
JASS:
struct A
endstruct
struct B
endstruct
private function init takes nothing returns nothing
local A a = A.create()
local B b = A.create() // no error ...
set a = 42 // no error ...
set a = "bla" // pjass error
endfunction
vJass doesn't check if a variable of type B only stores values of type B. This leads to frequent errors, which can be discovered late or you receive a pJass Error, which isn't easy to understand.
The Wurst counterpart (Screenshot from the Eclipse Plugin):
3. Verbose syntax
vJass features several redundant Syntax-Elements. For example "set", "call" or "takes nothing returns nothing". Additionally vJass doesn't have many features that allow for writing readable, clear code. Particularly there only exists one type of loop.
WurstScript features less verbosity, but without losing similarity to (v)Jass so switching is easier, and rapid code production for faster map creation.
Syntax
So how does it look like? Here is a simple spell to give you a first idea. The spell fires a missile to the target location, where it damages and knocks back all units in a certain range.
JASS:
package WarStomp
import ClosureEvents
import ClosureTimers
import Fx
import Knockback
import TempGroups
constant spellId = 'A001'
constant impactAoe = 600.
constant missileType = "AbilitiesWeaponsAncientProtectorMissileAncientProtectorMissile.mdl"
constant missileSpeed = 1000.
constant gravity = 981.
constant effectType = "AbilitiesSpellsHumanThunderclapThunderClapCaster.mdl"
constant effectType2 = "ObjectsSpawnmodelsUndeadImpaleTargetDustImpaleTargetDust.mdl"
constant maxSpellDamage = 100.
constant lvlDamageBonus = 25.
constant knockbackStrength = 700.
constant knockbackTime = 1.5
class WarStomb
unit caster
int spellLvl
Fx missile
vec3 missileVelocity
timer t = getTimer()
construct(unit caster, int spellLvl, vec2 targetPos)
this.caster = caster
this.spellLvl = spellLvl
vec2 casterPos = caster.getPos()
angle castDirection = casterPos.angleTo(targetPos)
real distToTarget = casterPos.distToVec(targetPos)
// the initial z-speed is calculated, so that the z-speed will be 0 after half the flytime
let flyTime = distToTarget / missileSpeed
let speedZ = (flyTime/2) * gravity
let missileVelocity2D = missileSpeed * castDirection.direction()
missileVelocity = missileVelocity2D.withZ(speedZ)
missile = new Fx(casterPos, castDirection, missileType)
missile.setScale(1.5)
t.setData(this castTo int)
t.startPeriodic(ANIMATION_PERIOD, function callMoveMissile)
static function callMoveMissile()
(GetExpiredTimer().getData() castTo WarStomb).moveMissile()
function moveMissile()
vec3 missilePos = missile.getPos3d()
missilePos += missileVelocity * ANIMATION_PERIOD
missile.setPos(missilePos)
missileVelocity.z -= gravity * ANIMATION_PERIOD
if missilePos.z <= 0
onImpact()
function onImpact()
let missilePos = missile.getPos2()
//create a big effect at location location of the missile
Fx fx = new Fx(missilePos, angle(0), effectType)
..setScale(2)
..flash(effectType2)
//destroy the effect after 2 seconds
doAfter(2, () -> destroy fx)
for unit u from ENUM_GROUP..enumUnitsInRange(missilePos, impactAoe)
let distToImpact = missilePos.distToVec(u.getPos())
let distanceFactor = 1 - (distToImpact / impactAoe)
//damage the unit relative to it's damage distance to the caster
let damage = (maxSpellDamage + spellLvl * lvlDamageBonus)*distanceFactor
caster.damageTarget(u, damage)
//knockback the target unit away from the impact location
new Knockback(caster, u, knockbackStrength * distanceFactor,
knockbackTime, missilePos.angleTo(u.getPos()))
destroy this
ondestroy
t.release()
destroy missile
init
//Creates a new instance of the class WarStomb if the spell is casted
onPointCast(spellId, (unit caster, int spellLvl, vec2 targetPos) ->
new WarStomb(caster, spellLvl, targetPos))
As you can see, Wurst has an indentation based syntax, and therefore you do not have to write words like endif or endfunction. The example also shows some of the syntactic sugar in Wurst, for example the for-from loop, update-assignments with +=, or the dot-dot-expression. You can also see some of the features like packages, classes, closures and tuples.
Features
The full list of features is documented in the Wurst Manual. Here we present only some selected features.
Local Type Inference + Local Variable Freedom
The type of local variables can automatically be derived from the initial value. Furthermore variables can be declared at any position in a function, not just the beginning.
JASS:
// "let" defines a local constant
let harald = CreateUnit(Player(0), 'hfoo', x, y, 0)
// "var" defines a local variable
var otto = CreateUnit(Player(0), 'hfoo', x, y, 0)
// traditional way works too
unit heinz = CreateUnit(Player(0), 'hfoo', x, y, 0)
We encourage you to use let whenever possible, because code is easier read when you know which variables are changed later and which variables keep their initial value.
Extension functions
Extension functions allow to add specific functions to an already existing type, which can be used via dot-syntax.
Declaration:
JASS:
public function unit.getOwner() returns player
return GetOwningPlayer(this)
function player.getName() returns string
return GetPlayerName(this)
Usage:
JASS:
print(GetKillingUnit().getOwner().getName() + " killed " +
GetTriggerUnit().getOwner().getName() + "!")
Extension functions have two pros in comparison to normal functions:
- Functions are easier to find using auto-complete in Eclipse
- In many cases the readability is improved, as chained calls are often easier to read than nested calls. With chained calls the order of execution is from left to right, while with nested calls the execution is from the inner to the outer:
JASS:// nested function calls: name = GetPlayerName(GetOwningPlayer(u)) // chained function calls name = u.getOwner().getName()
Tuples
A Tuple is a very simple Datatype. It allows to group several values into one. This allows treating the group of values as a group. Other than classes, tuples don't create any overhead. They don't have to be created or destroyed and can be used for primitive data types (int, string, etc.). A good example are vectors(from the stdlib):
Definition:
JASS:
// A 2d vector with the components x and y
public tuple vec2( real x, real y )
// Operator overloading functions:
public function vec2.op_plus( vec2 v ) returns vec2
return vec2(this.x + v.x, this.y + v.y)
public function vec2.op_minus( vec2 v ) returns vec2
return vec2(this.x - v.x, this.y - v.y)
public function vec2.op_mult(real factor) returns vec2
return vec2(this.x*factor, this.y*factor)
// dot product:
public function vec2.dot( vec2 v ) returns real
return this.x*v.x+this.y*v.y
// length:
public function vec2.length() returns real
return SquareRoot(this.x*this.x+this.y*this.y)
// normalized Vector:
public function vec2.norm() returns vec2
real len = this.length()
real x = 0
real y = 0
if (len != 0.0)
x = (this.x / len)
y = (this.y / len)
return vec2(x,y)
public function vec2.polarOffset(real angle, real dist) returns vec2
return vec2(this.x + Cos(angle)*dist, this.y + Sin(angle)*dist)
Usage:
JASS:
// A projectile homes the target unit(variable following)
function followHero()
// Increase angle
angle += TURN_SPEED*DT
// calculate new position
let newPos = following.getPos().polarOffset(angle, RANGE)
// current velocity is calculated from the difference between old and new position
vel = (newPos - pos)*(1/DT)
pos = newPos
fx.setPos(pos.x, pos.y)
SetUnitFacing(fx, (angle + bj_PI/2)*bj_RADTODEG)
checkCollisions()
// projectile moving forward
function moveForward()
// Add velocity to position
pos = pos + vel*DT
if not pos.inBounds()
destroyed = true
else
fx.setPos(pos.x, pos.y)
checkCollisions()
Anonymous functions / closures
With anonymous you can define a new function where it is needed. This is useful for timers, as shown in the following example, where the timer t is started with an anonymous function:
JASS:
class Fireball
timer t
//...
construct(unit caster, vec2 target)
// ...
t = getTimer()
t.setData(this castTo int)
t.start(0.05, () -> begin
let fireball = GetExpiredTimer().getData() castTo Fireball
fireball.move()
end)
function move()
// do stuff
Closures are more than just anonymous functions. They also capture variables. This allows to write some very concise code. For example it is easily possible to destroy an Fx after some time:
JASS:
let fx = new Fx(pos, facing, model)
fx.setScale(2.0)
// destroy fx after 2 seconds:
doAfter(2.0, () -> destroy fx)
Here the closure captured the local variable fx without the need to get a timer, attach stuff to the timer, and all that boilerplate code. The doAfter function is defined in the standard library and it can not only destroy Fx objects. You can write any code inside the closure and it will run after the given time.
Of course you can do more crazy stuff with closures. Here is the spell from the introduction, but implemented with closures instead of a class:
JASS:
package WarStompCl
import ClosureEvents
import ClosureTimers
import ClosureForGroups
import Fx
import Knockback
constant spellId = 'A001'
constant impactAoe = 600.
constant missileType = "AbilitiesWeaponsAncientProtectorMissileAncientProtectorMissile.mdl"
constant missileSpeed = 1000.
constant gravity = 981.
constant effectType = "AbilitiesSpellsHumanThunderclapThunderClapCaster.mdl"
constant effectType2 = "ObjectsSpawnmodelsUndeadImpaleTargetDustImpaleTargetDust.mdl"
constant maxSpellDamage = 100.
constant lvlDamageBonus = 25.
constant knockbackStrength = 700.
constant knockbackTime = 1.5
init
onPointCast(spellId, (unit caster, int spellLvl, vec2 targetPos) -> begin
let casterPos = caster.getPos()
let castDirection = casterPos.angleTo(targetPos)
let distToTarget = casterPos.distToVec(targetPos)
// the initial z-speed is calculated, so that the z-speed will be 0 after half the flytime
let flyTime = distToTarget / missileSpeed
let speedZ = (flyTime/2) * gravity
let missileVelocity2D = missileSpeed * castDirection.direction()
var missileVelocity = missileVelocity2D.withZ(speedZ)
let missile = new Fx(casterPos, castDirection, missileType)
..setScale(1.5)
doPeriodically(ANIMATION_PERIOD, (CallbackPeriodic cb) -> begin
var missilePos = missile.getPos3d()
missilePos += missileVelocity * ANIMATION_PERIOD
missile.setPos(missilePos)
missileVelocity.z -= gravity * ANIMATION_PERIOD
if missilePos.z <= 0
let impactPos = missilePos.toVec2()
//create a big effect at location location of the missile
let fx = new Fx(missilePos, angle(0), effectType)
..setScale(2)
..flash(effectType2)
//destroy the effect after 2 seconds
doAfter(2, () -> destroy fx)
forUnitsInRange(impactPos, impactAoe, (unit u) -> begin
let distToImpact = impactPos.distToVec(u.getPos())
let distanceFactor = 1 - (distToImpact / impactAoe)
//damage the unit relative to it's damage distance to the caster
let damage = (maxSpellDamage + spellLvl * lvlDamageBonus)*distanceFactor
caster.damageTarget(u, damage)
//knockback the target unit away from the impact location
new Knockback(caster, u, knockbackStrength * distanceFactor,
knockbackTime, impactPos.angleTo(u.getPos()))
end)
destroy missile
// stop periodic call:
destroy cb
end)
end)
Please note, that this coding style is not recommended. Dividing code into several named functions is much more readable. Also the performance is worse, when using closures.
Compiletime functions
You can execute Wurst code at compile-time. Wurst includes some pseudo-natives which can be executed at compile-time to generate object-editor entries. This is similar to what can be done with the ObjectMerger tool, but it also comes with nice readable method names. Here is an example which creates a spell based on Channel and a tower unit for each level:
JASS:
@compiletime function generateSpell()
// based on shadow hunter serpent wards
int levelCount = 4
let def = new ChannelSpellPreset(GUN_TURRET_SPELL_ID, levelCount)
..setName("Gun Turret")
..setIcon("ReplaceableTexturesCommandButtonsBTNElvenGuardTower.blp")
..setIconNormal("ReplaceableTexturesCommandButtonsBTNElvenGuardTower.blp")
..setTargetType(Targettype.PTARGET)
..setOption(Option.VISIBLE, true)
..setFollowThroughTime(0.4)
..setDisablesOther(false)
for lvl = 1 to levelCount
createTower(lvl)
def.setCastingTime(lvl,0)
def.setCastRange(lvl, 300)
function createTower(int lvl)
// based on high elven guard tower:
let def = new BuildingDefinition(GUN_TURRET_TOWER_ID + lvl, 'negt')
..setName("Gun Turret Level " + lvl.toString())
..setHitPointsMaximumBase(100 + lvl*100)
// no projectile
def.setAttack1ProjectileArt("")
// 0 damage
def..setAttack1DamageBase(-1)
..setAttack1DamageNumberofDice(1)
..setAttack1DamageSidesperDie(1)
..setAttack1CooldownTime(attackCooldown(lvl))
..setAttack1Range(attackRange)
..setScalingValue(0.8)
..setGroundTexture("")
It is even possible to share code between compile-time functions and normal in-game functions. For example the constant GUN_TURRET_SPELL_ID or the function attackCooldown can be used to configure the generated objects and they can be used in the code for the spell.
Optimizer
Wurst has an integrated optimizer, with several optimizations:
- inline functions (can inline most functions, exception are multiple return statements in a function)
- inline constants
- simple local optmizations (e.g. optimize
3*4
to12
or removeif false ...
) - name-compression
- tree-shaking (remove unused variables and functions)
- automatic null-setting of handles
Eclipse Plugin
(Click to enlarge)
Run your map from eclipse
You can compile all scripts and run your map directly from eclipse. This can be much faster than saving the map in WorldEditor first, because only the script is updated and eclipse already has the script in memory.
Live errors and warnings
Errors and warnings are directly shown in your code without the need for pressing compile or saving the whole map.
Context-sensitive auto-complete
If you have an expression with a dot, then auto-complete will show you all members available for the given type. This is especially useful when used with extension methods, because it is very easy to find the function you are looking for.
Hotdoc
As already seen in the auto-complete screenshot, Wurst has support for documenting functions with so called Hotdoc comments. These comments are shown in auto-complete and when you hover your mouse above a function call.
Jump to declaration
Hold down the Ctrl key and click on a variable or function and you will directly jump to the place where it is declared.
Console and REPL
You can use the console to evaluate expressions and test your code in eclipse, without even starting WarCraft. Of course this is limited, as not all natives are implemented in the Interpreter, but it is nice for testing data-structures or mathematical calculations.
Other benefits of using eclipse
Many features are already available in Eclipse and not specific to the Wurst-Plugin:
- Press Ctrl+Shift+R to quickly jump to a file. It even supports using wildcards for searching files.
- Powerful search (and replace) in all files.
- Integration with version control (Git & co).
- ...
Manual
Installation / Download
Please find the installation instructions on GitHub.
Other links:
Youtube Tutorials:
Download, Installation & Setup | Eclipse & WorldEditor workflow |
Projects using Wurst
- http://www.hiveworkshop.com/forums/map-development-202/heart-night-225166/
- http://www.hiveworkshop.com/forums/maps-564/escape-builder-r-0-82z-184964/
- https://github.com/Frotty/24Challenge
- https://github.com/Frotty/CastleAttack
- https://github.com/Frotty/ForestDef
- https://github.com/Frotty/InWcSchneeball
- https://github.com/Ruk33/Phoenix-AI---Concurso-WW
- https://github.com/muzzel/Momentum
FAQ (Frequently asked questions):
Can I use vJass together with Wurst?
Yes, you can use both. However it is not possible to call Wurst-functions from vJass and calling vJass from Wurst is not very convenient.I have further problems, suggestions or questions. Where can I get help?
- In this thread.
- Create a ticket on github.
- Private message to peq or Frotty
- quakenet IRC #inwc.de-maps (German chat where you can usually find us)
Last edited: