The Video
Where To Go From Here
Take a look around at the .js
files in the scriptcraft/plugins directory.
My Notes for the Video
The following sections are my unedited notes that I wrote before making the video.
Part 6: More JS!
In the last part, we broke free from the command line, opening up all kinds of new possibilities. In this part, we’re going to learn more features of JavaScript that we can use to bend Minecraft to our will.
But first, debugging
When you’re writing code, inevitably something is going to go awry. JSHint will help you catch certain kinds of errors, but there are many others that it will miss.
One way to debug is to try things out in Minecraft with the command line. If you can double check a little piece of code there, that may help you.
Probably the easiest and most powerful tool in your toolbelt is echo
. Let’s look at mycottage from last time:
/*global require, echo*/
var Drone = require("drone");
var blocks = require("blocks");
function mycottage() {
this
.chkpt("cottage")
.down()
.box(blocks.birch, 7, 1, 6) // birch wood floor
.up()
.box(blocks.air, 7, 5, 6) // clear area first
.box0(blocks.moss_stone, 7, 2, 6)
.right(3)
.door()
.up(1)
.left(2)
.box(blocks.glass_pane)
.right(4)
.box(blocks.glass_pane)
.left(5)
.up()
.prism0(blocks.stairs.oak, 7, 6) // add a roof
.down()
.right(4)
.back()
.wallsign(["My", "Cottage"])
.move("cottage")
.right(3)
.fwd(4)
.up()
.hangtorch()
.move("cottage")
.right()
.fwd(3)
.bed()
.fwd()
.right(4)
.box(blocks.furnace)
.move("cottage");
}
Drone.extend(mycottage);
When ScriptCraft loads this script, it runs the code outside of the mycottage
function at the time it loads. The code inside of mycottage
only runs when you call it on the Drone. That means that adding echo
calls outside of mycottage
will tell you if you’ve got a problem even telling ScriptCraft about mycottage
. If the cottage doesn’t build correctly, you’ll want to add echo
inside. Here’s an example:
/*global require, echo*/
echo("Going to require other modules");
var Drone = require("drone");
var blocks = require("blocks");
echo("Modules loaded");
function mycottage() {
this
.chkpt("cottage")
.down()
.box(blocks.birch, 7, 1, 6) // birch wood floor
.up()
.box(blocks.air, 7, 5, 6) // clear area first
.box0(blocks.moss_stone, 7, 2, 6)
.right(3)
.door()
.up(1)
.left(2)
.box(blocks.glass_pane)
.right(4)
.box(blocks.glass_pane)
.left(5)
.up();
echo("I think something is going on with the roof");
this
.prismo(blocks.stairs.oak, 7, 6); // add a roof
echo("Did the roof draw okay?");
this
.down()
.right(4)
.back()
.wallsign(["My", "Cottage"])
.move("cottage")
.right(3)
.fwd(4)
.up()
.hangtorch()
.move("cottage")
.right()
.fwd(3)
.bed()
.fwd()
.right(4)
.box(blocks.furnace)
.move("cottage");
}
Drone.extend(mycottage);
echo("Drone extended, ready to build the cottage!");
In order to put some echo
calls in the middle of the Drone’s drawing, I needed to break up those chained function calls. Those chains().of().calls().strung().together()
is a feature of the Drone in particular, and echo
isn’t a part of that.
So, I added a ;
after that up()
call, put the echo
call after that (with a ;
terminating that, as is normal for JavaScript statements), and then added another this
because I needed to restart the chain where the Drone was located.
commands and function arguments
With a quick debugging tutorial out of the way, let’s go back to adding things to Minecraft.
You can create special “jsp” commands using the command
function. These commands are called with two arguments. When we’ve called functions before on the Drone, we’ve included arguments. When you want to write a function that takes arguments, you give each one a name based on what is going to be passed in when the function is called. ScriptCraft commands are automatically called with two arguments: parameters
and player
. parameters
is each of the words you type in when you run the command. It’s an array. player
is an object with all kinds of stuff on it, as we’ll see shortly.
Let’s write a command. Create a new file in your scriptcraft/plugins
directory called “mycommands.js”. This is what goes into it:
/*global require, echo, command*/
command("hi", function (parameters, player) {
var salutation = parameters[0];
echo(player, salutation + " " + player.name);
});
parameters
is an array, which is like a list of values.- You can get to any item in the list by using square brackets.
- The first item in the list is 0.
- We’ve seen
echo
before, but here we’re using it with the player object as the first argument - That means that the message will only be displayed to that player
- In JavaScript, you can use
+
to join strings together. So, we join together the salutation and the player’s name.
Now we try it. In Minecraft, type /jsp hi Amazing
.
Conditionals and doing stuff with the player object
In Minecraft, try this:
/js self.location.world.setRaining(true)
Pretty neat to control the weather, eh? Let’s turn the rain back off.
That’s a bit inconvenient to type though. Wouldn’t it be great to have a command to control the weather?
command("rain", function (parameters, player) {
player.location.world.setRaining(true);
});
With that, we have a rain command that makes it rain. It would be nice to be able to turn it off as well. We could make a command called rainoff
that does the opposite. Or we could add a parameter to rain
that turns it off, if that parameter is there.
So far, all of our code has been “do this, then do that, then do the other”. Now we want a command that does something if a certain parameter is there. And if
is the name of the JavaScript keyword we want.
command("rain", function (parameters, player) {
if (parameters[0] === "off") {
player.location.world.setRaining(false);
} else {
player.location.world.setRaining(true);
}
});
- use
if
followed by parenthesis that contain the condition that is being checked. - In JavaScript, you use
=
to assign a value to a variable. ==
and===
both becometrue
orfalse
depending on whether or not there’s a match==
has some possibly surprising behavior, so I usually just use===
. The details aren’t important.- The first block of code after the
if
is going to run if that expression is true. - The expression is
parameters[0] === "off"
. So, if the parameter you type in is the wordoff
, that will be true and we’ll callsetRaining(false)
. - After that, we use the
else
keyword. That’s followed by a block that is run if the original expression is false. You don’t have to do an else, but that’s what we want in this case. - Just like with functions, we put those blocks in curly braces.
Give it a try with /jsp rain
and /jsp rain off
.
for loops
Another new idea along the lines of stretching beyond “do this then do that” is the notion of “do this 5 times then do that”. Of course, if you want to do something five times, you could just write out that set of commands five times. Or, you could make a function that does what you want, and then call that five times.
But, what if you want to do something 100 times. That’s getting to be a bit much to type out. Or, what if you don’t know how many times you want to do something at the time you write the code?
As an example, let’s make a Drone function called torchwall
that creates a wall that has torches. You’ll be able to pass in the width and height of the wall.
function torchwall(width, height) {
this.chkpt("wall");
this.box(blocks.stone, width, height, 1);
this.move("wall");
}
Drone.extend(torchwall);
That basic scaffolding will give us a stone wall with the width and height that are passed in. How would we add torches along this wall when we don’t know how big it is? Let’s start by adding one torch:
function torchwall(width, height) {
this.chkpt("wall");
this.box(blocks.stone, width, height, 1);
this
.up()
.back()
.hangtorch()
.fwd()
.down()
.right();
this.move("wall");
}
Drone.extend(torchwall);
That places a torch one block up on the wall and then moves the Drone back to the bottom. If we could just repeat that, we’d have torches along the wall.
Here’s how we use a for
loop to do that:
for (var counter = 0; counter < width; counter++) {
this
.up()
.back()
.hangtorch()
.fwd()
.down()
.right();
}
Note that in Brackets and other editors, you can select a few lines of code and use the Indent or Unindent command in the Edit menu to move the code. Take a look at the edit menu and you’ll likely find a lot of stuff that will come in handy.
So, the for
keyword takes a bunch of crazy looking stuff and then has a block of code that it will repeat some number of times. Let’s break apart that for
:
for (var counter = 0; counter < width; counter++)
We’ve seen something like var counter = 0
before. This does exactly what it looks like: before it starts looping, it sets a variable called counter
to 0. The next part is a check that it does before each run of the loop. This one says to see if counter is less than width, so as long as the current value of counter
is less than width, it will run the code to add a torch. The last part, counter++
is what happens at the end of the loop. In JavaScript, you can use ++
as a quick way to add one to a number (this is called incrementing).
Let’s step through what happens if the width
is 3.
counter
is set to 0- Is
counter
less than the width (3)? Yes it is, so add a torch (which ultimately causes the Drone to move 1 block to the right) - Increment
counter
so that it becomes 1. - Is
counter
(now 1) less than the width (3)? Yes it is, so add another torch. - Increment
counter
so that it becomes 2. - Is
counter
(now 2) less than the width (3)? Yes it is, so add another torch. - Increment
counter
so that it becomes 3. - Is
counter
(now 3) less than the width (3)? No! So, the loop stops and we start running the code just after thefor
loop’s block.
The code makes a wall that is 3 blocks wide and places 3 torches. When you list out all of the little things a computer has to do, it seems like a lot. But, modern computers do literally billions of things per second.
Let’s try it:
/js refresh()
/js up().torchwall(10, 3)
It seems a bit much to put torches all along the wall, right? We can make it so that you can space them out with an extra argument to the function.
function torchwall(width, height, torchspacing) {
this.chkpt("wall");
this.box(blocks.stone, width, height, 1);
for (var counter = 0; counter < width; counter += torchspacing) {
this
.up()
.back()
.hangtorch()
.fwd()
.down()
.right(torchspacing);
}
this.move("wall");
}
Let’s erase our old wall and try again.
/js refresh()
/js box(10, 3, 1)
/js up().torchwall(10, 3, 3)
We add a torchspacing
argument and change the last part of the for
loop to counter += torchspacing
. +=
is a nice shorthand for saying counter = counter + torchspacing
, which boils down to “add torchspacing to counter”. The final change is at the end of the for
loop’s code block, we move right
by torchspacing
rather than just 1.
So, if torchspacing
is 3, we’ll move right by 3 at the end of that block. As before, we stop placing torches once counter
is greater than or equal to width
, so if the width
is 3 we’ll only place a single torch. If it’s 9, we’ll place 3 torches.
Putting it all together
Here’s a couple of quick Brackets tricks. You can open another file by tracking it down in the file tree over here. But, you can also use Quick Open from the Navigate menu. It’s cmd-shift-O on the Mac and ctrl-shift-O on Windows. You just start typing. I’ll type “cottage” and then open the cottage.js file that comes with ScriptCraft.
Also under the Navigate menu is “Quick Find Definition”. This lets you jump to a function quickly. I’ll select cottage_road.
I don’t know how new cottage_road
is, but I only noticed it recently and it’s very cool. Let’s try it.
/js cottage_road(6)
This gives us 6 cottages along a nice street.
There’s a lot going on in this code and it takes a good deal of practice to write code like this. But, I want to talk through it so that at least you’ll be able to read it and have an idea of what it’s doing.
function cottage_road( numberCottages ) {
if (typeof numberCottages == 'undefined'){
numberCottages = 6;
}
JavaScript has a keyword, typeof
that takes a variable and tells you what kind of variable it is, whether it’s a number, array, object or, in this case, “undefined”. undefined
is a special value in JavaScript. If you don’t pass in an argument to this function, numberCottages
will be undefined. If it is undefined, this code will set numberCottages
to 6.
var i=0, distanceBetweenTrees = 11;
//
// step 1 build the road.
//
var cottagesPerSide = Math.floor(numberCottages/2);
We define a couple of variables that we’ll use. Math
is an object that comes with JavaScript that includes a number of useful math functions. The Math.floor
function rounds a number down to the previous integer. If numberCottages
is 6, then you divide 6 by 2 and get 3. 3 is already an integer, so Math.floor
doesn’t change it. If numberCottages
is 7, then you divide 7 by 2 and get 3.5. That’s not an integer, so Math.floor
basically just chops off anything after the decimal point, rounding the number down to 3.
this
.chkpt('cottage_road') // make sure the drone's state is saved.
.box( blocks.double_slab.stone, 3, 1, cottagesPerSide * ( distanceBetweenTrees + 1 ) ) // build the road
.up()
.right() // now centered in middle of road
.chkpt('cr'); // will be returning to this position later
This is just like all of the drawing code we’ve seen before. It creates the road.
for ( ; i < cottagesPerSide+1;i++ ) {
this
.left(5)
.oak()
.right(10)
.oak()
.left(5) // return to middle of road
.fwd( distanceBetweenTrees + 1 ); // move forward.
}
Now, we’ve got a for
loop, just like we’ve been discussing. This one didn’t need to initialize anything, so there’s just a ;
after the left parenthesis. This loop will put one more tree per side than there are cottages per side, because of the +1
there. The body of the loop just goes to the left, places an oak, goes to the right, places an oak, returns to the road and then moves forward by the distanceBetweenTrees
amount that was computed earlier.
this
.move('cr')
.back(6); // move back 1/2 the distance between trees
More standard drawing stuff.
function pathAndCottage( drone ) {
drone
.down()
.box(blocks.double_slab.stone, 1, 1, 5)
.fwd(5)
.left(3)
.up()
.cottage();
return drone;
};
This is interesting… there’s a function inside of this function. This is perfectly legal in JavaScript and is a nice way to split things up sometimes. The pathAndCottage
function is only available to code within the cottage_road
function. The cottage
function can’t use it, because it’s tucked away inside of cottage_road
.
pathAndCottage
is given the drone to draw with and then it puts a path to the cottage and places the cottage itself using normal drone drawing functions.
Another for
loop:
for ( i = 0; i < cottagesPerSide; i++ ) {
this
.fwd( distanceBetweenTrees + 1 )
.chkpt('r'+i);
// build cottage on left
pathAndCottage( this.turn(3) ).move( 'r' + i );
// build cottage on right
pathAndCottage( this.turn() ).move( 'r' + i );
}
This starts by resetting i
, which is a common variable name used for counters in for
loops. It moves forward, keeps a checkpoint with a name of 'r'+i
. This means that the first checkpoint will be called “r0”, the next is “r1” and so on. Then you can see why pathAndCottage
is a function: it’s used to place the cottages on the right and left. If pathAndCottage
wasn’t a function, then there would probably be two copies of the code to draw the path and cottage. That would work fine, but you’ll find it’s often a good idea to make a function rather than duplicating some code.
And, we end the function like most of our drawing code:
// return drone to where it was at start of function
this.move('cottage_road');
Look around!
Take a look around at the .js
files in the scriptcraft
folder. Especially look in scriptcraft/plugins/drone/contrib
where you can see how much of the cool stuff on the drone is built. You can even copy and paste code from there as a starting point!
debugging, if and for
In this part, we covered debugging, if
and for
. That’s a pretty powerful collection of tools for your JavaScript toolbelt. When you use a hammer for the first time, the nail probably isn’t going to go in totally straight… using these tools that the JavaScript language gives us takes some practice to get right, but give it a whirl!