Stage 8 - Spells
If you try playing the game a bunch you might notice it's weighted a bit in the monster's favor. The player bump attack is limiting and not especially exciting. In my opinion, what really makes a broughlike special is your abilities or spells. You can see this especially in 868-HACK and Cinco Paus. Proper use of spells is what adds most of the depth and personality.
There's 15 spells in this section to demonstrate the diversity of what can be done with very little code. What I want to stress is: it's your game now. Do all 15 if you want, or pick and choose a few, or go off and implement something completely different. It's up to you.
First, let's write a single spell to give you an idea of what that looks like. Then we'll knock out the framework for casting spells and lastly I'll show you the rest of the 15 spells one by one.
spell.js
We're going to store our spells in an object literal called, unsurprisingly,
.
warps the player to a random passable tile. This
function has a one line body. Pretty simple huh?
Notice the function operates on the
object. All of our spells will be player-cast, but it's not hard at all to have spells castable by either players or monsters by passing in a "caster" entity and applying everything to that. I'll leave that as an exercise for the reader.
We're going to let the player select spells with the number keys 1-9.
index.html
Here's an interesting example of JavaScript's type coercion. The value
will come in as a string like "2".
Is the string "2" greater than or equal to 1? Strictly speaking, that doesn't make any sense, but JavaScript will try to make it work anyway. Your "2" will be coerced into the number 2 and then you
can try comparing it with 1.
So if the pressed key is 1-9, we're passing that key number minus 1 (type coercion comes into play yet again) to a new function called
. That number will represent an index into our array of spells. We're subtracting 1 because array indices start at 0 instead of 1.
Spell framework
Now we'll add the code to load the player spells, add a new spell, and cast them. The player will initially start out with a single spell and will gain additional spell slots through acquiring treasure.
First let's initialize
so that the player starts out with a single spell.
game.js
Then the bulk of the code to handle adding/casting spells:
monster.js
It's sometimes confusing to see different variables with the same name, so let's make sure we understand the difference. The global object
holds the spell functions. The player
spells
(shown above as
)
is an array of spell
names, which we can use to index into the global object. It's an inventory of sorts.
The new line in our player constructor does quite a bit...
-
gets all the spell names using
-
shuffles them
-
grabs some number of spells from that shuffled list using
-
assigns them to
The method
looks similar, but it only grabs one random spell and then adds it to the player spells array.
The method
takes an index (remember the player pressing 1-9 earlier?) and tries to find that index in the player spells array. It may not exist, which is OK! That's why we do a check on the result.
If found, we
the element and leave an empty array slot. We call the spell function, play our spell sound, and do a
.
You could also skip the tick if you don't want monsters to act after a spell is cast.
Drawing spells
Now let's draw our spell list on the sidebar:
game.js
If your recall the way we used
before, this should be pretty straightforward... with the exception of the expression:
(i+1) + ") " + (player.spells[i] || "")
We're adding 1 back to our spell index to make it like normal human counting, adding a parentheses and space, and then adding the spell name.
If the spell has been deleted, we want to handle that with the "OR" operator
and instead simply add an empty string. You don't need to know all the details of how
works here (some more type coercion is involved), but I will say it's kind of like the English word "or". Do the first thing
or, if that doesn't work, do the second thing.
The result with our first spell should be:
Test out that code and you should see the above
and you should be able to cast your first spell.
Gaining new spells
Let's connect spells to treasure.
tile.js
Every 3 treasures acquired results in a new spell all the way up to 9 slots.
So that's the framework in a nutshell.
Spell 2: QUAKE
For this spell only, I'll show the surrounding code. But all the spell functions will be added as properties of the
object in the same way: add a comma after the last function, break to a new line, then add the new function.
Later, if things start breaking the first thing you should check is your commas!
spell.js
iterates over each tile and, if a monster is present, deals it 2 damage for each adjacent wall. We're reusing the screenshake and it's way more satisfying here.
One note on testing. You might want to set
initially to 9 while testing so you have easier access to the available spells on the first level.
Spell 3: MAELSTROM
spell.js
iterates over all monsters and teleports them to a random tile just like
did for the player. Then it sets the monster teleport counter, so we get the same behavior as when monsters spawn in the first time.
Spell 4: MULLIGAN
spell.js
game.js
resets the level without increasing the level count or resetting the player's spells and sets the player's HP to 1. This is the first of many spells that can be used to farm treasure.
Spell 5: AURA
spell.js
heals both the player and any adjacent monsters. It also uses a new tile method
which will add a short lived sprite to that tile.
Effects
We're going to draw 4 sprites for effects.
Our heal effect is just a few bright green circles with a couple scattered green pixels.
Explosion effects are usually made by drawing concentric bubbly shapes that are white, yellow, orange, red, and black.
We make a bolt effect by drawing a wiggly line, outlining it in white, and then doing some basic antialiasing to smooth it out (simply drawing a midpoint color between the two in a few spots).
Rotate 90 degrees to make the same effect in a vertical orientation.
Now the code to draw effects.
tile.js
In
,
we simply save the passed in
for later use.
And we set
to 30, which determines the length of the effect in frames (30 frames should be about half a second).
As long as
is greater than 0, we decrement its value, and then draw the effect sprite.
Now here's something we haven't seen before:
.
This lets us control the transparency of all sprites drawn. A value of 1 means fully opaque and 0 means fully transparent. By setting it to
we ensure that the effect fades out. We reset the global alpha to 1 at the end to avoid affecting any other sprites.
Spell 6: DASH
spell.js
moves the player in the direction of their last move or attack until they are blocked by a wall or monster. If the player was able to move, adjacent monsters are damaged, they are stunned, and an effect is drawn.
We're using
to move in the
direction (defined below) one tile at a time in a
loop. It's a common approach to use
until you meet some condition and then
out of the loop. We'll use it again later. Just be careful when writing such a loop that you have a proper way to break out; otherwise you can easily end up in an infinte loop and crash your browser!
Here's the code to track
.
monster.js
Spell 7: DIG
spell.js
replaces all walls (not including the outer wall) with floors. The player is healed for 2 health and an effect is drawn on the player tile.
Spell 8: KINGMAKER
spell.js
heals all monsters and generates a treasure on their tile.
Spell 9: ALCHEMY
spell.js
turns all adjacent walls that are not part of the outer wall into floors with a teasure.
Spell 10: POWER
spell.js
monster.js
makes the next player attack do 6 damage by using a new variable called
.
Spell 11: BUBBLE
spell.js
duplicates spells. It iterates over the player spells in reverse and copies a spell from the previous element if the current element is empty.
Spell 12: BRAVERY
spell.js
gives the player a free turn by iterating over all monsters and stunning them.
It also adds a new property called
,
which will prevent any damage until the turn after next.
To support the
property, we need to do three things:
-
prevent the player from taking damage by returning early in the
method if the
is greater than 0
-
adding a player version of
that decrements the
every turn
-
calling that
method
The reason we have two distinct versions of
is that player will never need regular monster AI behavior, but we still need a place to update player variables once per turn.
monster.js
game.js
Bolt travel
The last three spells use a function that we'll add to the end of spell.js called
.
spell.js
This function is sort of like
earlier. We pass in a specified
,
an
sprite, and some
number. Starting from the player tile, we move in that direction until we hit a wall. We draw an effect for each tile passed through and for each monster, we damage it.
We can do a lot with
.
Spell 13: BOLT
spell.js
First, a simple test of our new function with the aptly named
.
Like
earlier, this spell operates in the direction of the player's last move. It's somewhat hard to pull off, so we make it do 4 damage!
The effect expression might look a little bit weird, but all we're trying to accomplish is to return either 15 or 16 (15 is the location of our horizontal bolt sprite, 16 holds the vertical version). A horizontal last move will have 0 in
,
but a vertical move will have either -1 or +1. Taking the absolute value gives us 1 in either case.
Spell 14: CROSS
spell.js
also
calls
but in the 4 cardinal directions. We define these
in an array literal and iterate over it. We deal 2 damage and we use the same trick as last time to distinguish between horizontal and vertical bolts.
Spell 15: EX
spell.js
is pretty much the same as
, but in diagonal directions. We deal 3 damage and pass a single sprite that works for any direction.
The End
And with that, our spells are done. The whole thing is done. You've made a complete game in less than a thousand lines of code with no frameworks. Great job!
So...
what's next?