Dungeon 66
Damage display, and what about showing attacks? I have a few weak ideas …
The exploding snake was fun, but maybe we can do something a bit more sensible to show when a monster is hit. In fact, I know we can, and I know what it is. I’ve also been thinking that it would be nice if we could show some kind of attack coming from the attacker, and there I don’t have so much of an idea. There’s an issue that would have to be dealt with, which has showed up in the past. The screen is draw tile by tile, and when a tile is drawn, so are its contents, including the player or monster.
This gives us a bit of a problem. Suppose we wanted to show the princess swinging a sword down onto a monster to screen right. We could draw the sword to overlap both tiles easily enough. But the tile to her right is drawn after her tile, which means that when we draw that tile, it will cover up her sword.
There are at least two ways we might deal with that:
- Z level. It is possible to draw things at different z heights, where z is the axis in and out of the screen. Larger zLevel means “on top”. This would be great if it would work. I’ve had it fail in the past, but we use zLevel now to draw the character sheets, and they stay on top. So zLevel might work.
- Drawing tile contents, at least live contents, in a separate drawing pass. This would have a similar effect, since, all things being equal, drawing later equals drawing on top. It would be a pain to do, however. No one likes adding another pass over the data, even if we were to cache it.
We’re going to assume until proven wrong that we can use zLevel if we need it, and we don’t need it yet.
But those hits …
The Hits Keep Coming
The monsters all have a “hit” image, which is a plain white silhouette of the monster in question. And, even better, we’re actually importing its name in the monster tables:
...
m = {name="Serpent", health=1, strength=3,
dead=asset.snake_dead, hit=asset.snake_hit,
moving={asset.snake, asset.snake_walk}}
table.insert(MT,m)
...
So let’s change monsters to display their hit form, perhaps tinted, when the showDamage
flag is set. Draw looks like this now:
function Monster:draw(tiny)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
sprite(self.moving[self.movingIndex], 0,0)
if self.showDamage then
sprite(asset.builtin.Tyrian_Remastered.Explosion_Huge,0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
translate(-center.x,-center.y)
popStyle()
popMatrix()
self:drawSheet()
end
A simple change for this new scheme …
function Monster:draw(tiny)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
if self.showDamage then
tint(255,255,0)
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
translate(-center.x,-center.y)
popStyle()
popMatrix()
self:drawSheet()
end
I expect that’ll work just fine …
Yes, that should be just fine. We could get a bit more fancy. Let’s review the actual encounter coroutine.
function attackStrikes(attacker,defender, random)
local damage = rollRandom(attacker:strength(), random)
if damage == 0 then
yield("Weak attack! ".. defender:name().." takes no damage!")
if math.random() > 0.5 then
yield("Riposte!!")
firstAttack(defender,attacker, random)
end
else
defender:displayDamage(true)
yield(attacker:name().." does "..damage.." damage!")
defender:displayDamage(false)
defender:damageFrom(attacker.tile, damage)
if defender:isDead() then
yield(defender:name().." is dead!")
end
end
end
The action is in the lower part of this method, where we tell the defender to displayDamage. This happens before we apply the damage, which may or may not result in the death of the defender.
What if we were to display damage in yellow, unless the monster is down to only one or two health points, in which case we were to display red? We’ll try it. But a lot of the monsters, at least now, have only one health point, so they’ll show red and then die. I guess that’s OK. It’s easy to do while we’re here and we can see how it feels. Let’s make the threshold 2, so that you might see a flash of red but fail to kill the creature.
...
if self.alive then
if self.showDamage then
self:selectTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
...
function Monster:selectTint()
if self.healthPoints <= 2 then
tint(255,0,0)
else
tint(255,255,0)
end
end
Yes, that’s a bit better. I’d like to have a similar effect for the Princess. I don’t have a plain white silhouette for her, but I’ll make one offline. Maybe I’ll even write up how I did it: it’ll be easy.
Note that I made a little function for selectTint
rather than nest ifs even further. Let’s commit this as working, and then make this method more nearly right. Commit: monsters flash yellow or red with damage.
Now then:
function Monster:draw(tiny)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
self:drawSprite()
popStyle()
popMatrix()
self:drawSheet()
end
function Monster:drawSprite()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
if self.showDamage then
self:selectTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
translate(-center.x,-center.y)
end
I really shouldn’t have to do that reverse translate. There are some pushes and pops outside that drawSprite
function that protect us, but I prefer that anything that manipulates the graphical settings does its own save and restore. So …
function Monster:drawSprite()
pushMatrix()
pushStyle()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
if self.showDamage then
self:selectTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
popStyle()
popMatrix()
end
That still seems a bit much to me. Let’s extract again. I need a better method name, but …
function Monster:drawSprite()
pushMatrix()
pushStyle()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
self:drawCurrentStatus()
popStyle()
popMatrix()
end
function Monster:drawCurrentStatus()
if self.alive then
if self.showDamage then
self:selectTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
end
I still hate that nested if. Further extraction seems fraught. How about the tint, though?
function Monster:drawCurrentStatus()n
self:selectTint()
if self.alive then
if self.showDamage the
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
sprite(self.dead)
end
end
function Monster:selectTint()
if self:isDead() then
tint(0,128,0,175)
elseif self.healthPoints <= 2 then
tint(255,0,0)
else
tint(255,255,0)
end
end
Now that suggests this:
function Monster:drawCurrentStatus()n
self:selectTint()
self:selectSprite()
end
function Monster:selectSprite()
if self.alive then
if self.showDamage the
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
sprite(self.dead)
end
function Monster:selectTint()
if self:isDead() then
tint(0,128,0,175)
elseif self.healthPoints <= 2 then
tint(255,0,0)
else
tint(255,255,0)
end
end
I might like these two methods to look similar (though that way lies trouble. Let’s see:
function Monster:selectSprite()
if self:isDead() then
sprite(self.dead)
elseif self.showDamage then
sprite(self.hit)
else
sprite(self.moving[self.movingIndex])
end
end
function Monster:selectTint()
if self:isDead() then
tint(0,128,0,175)
elseif self.healthPoints <= 2 then
tint(255,0,0)
else
tint(255,255,0)
end
end
However, a quick test tells me that I’ve broken the tint. I’ve lost the check for showing damage. That requires me to do more than I like, and I don’t have a good save point. Revert, look again.
We’re back to this:
function Monster:draw(tiny)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
if self.showDamage then
self:selectTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
translate(-center.x,-center.y)
popStyle()
popMatrix()
self:drawSheet()
end
Let’s do the central extract again and then commit. But let’s do it a bit differently:
function Monster:draw(tiny)
if tiny then return end
self:drawMonster()
self:drawSheet()
end
function Monster:drawMonster()
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
if self.showDamage then
self:selectTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
else
tint(0,128,0,175)
sprite(self.dead)
end
translate(-center.x,-center.y)
popStyle()
popMatrix()
end
This time I pulled out everything that was about drawing the monster, including the pushes and pops and settings. We can delete the second translate, then commit: refactoring Monster:draw.
Lots of tiny saves, that’s always the ticket. That’s a habit I’ve not developed in myself, and I’d do well to improve on it.
Now invert that if:
function Monster:drawMonster()
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self:isDead() then
tint(0,128,0,175)
sprite(self.dead)
elseif self.showDamage then
self:selectDamageTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
popStyle()
popMatrix()
end
I also renamed the selectTint
:
function Monster:selectDamageTint()
if self.healthPoints <= 2 then
tint(255,0,0)
else
tint(255,255,0)
end
end
That’ll do for now. Commit: refactor monster drawing.
Today is Saturday, and my dear wife is making breakfast. Let’s take a short break to break our fast with breakfast.
Before I step away: I do have a small concern. We are not explicitly setting the tint here. We should.
function Monster:drawMonster()
pushMatrix()
pushStyle()
spriteMode(CENTER)
noTint()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self:isDead() then
tint(0,128,0,175)
sprite(self.dead)
elseif self.showDamage then
self:selectDamageTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
popStyle()
popMatrix()
end
Commit: ensure tint off in monster drawing.
Princess’s Damage
I’ve created a new graphic for the Princess’s damage display. I did it by opening the original princess graphic in Procreate, putting a new layer on top, setting it to Clipping Mask, and drawing in white all over the princess except for her crown and necklace. The results look like this:
Then I updated Player:draw
:
function Player:draw(tiny)
local dx = 0
local dy = 10
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self:graphicCenter() + vec2(dx,dy)
translate(center.x,center.y)
if not self.alive then tint(0) end
if tiny then
tint(255,0,0)
sx,sy = 180,272
else
sx,sy = 66,112
end
if self.showDamage then
self:selectDamageTint()
sprite(asset.Character_Princess_Girl_Hit,0,0,sx,sy)
else
sprite(asset.Character_Princess_Girl,0,0,sx,sy)
end
popStyle()
popMatrix()
if not tiny then
self.playerSheet:draw()
end
end
function Player:selectDamageTint()
if self.healthPoints <= 2 then
tint(255,0,0)
else
tint(255,255,0)
end
end
Now I’ll commit: Princess shows damage in yellow and red.
You’ll perhaps note that I used the translate
operation in this draw, similar to the one in Monster, except that the princess gets an offset to make her better centered in her tile.
Now let’s refactor a bit. This function doesn’t look as much like the Monster one as I’d like, but we can improve it nonetheless.
function Player:draw(tiny)
local sx,sy
local dx = 0
local dy = 10
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self:graphicCenter() + vec2(dx,dy)
translate(center.x,center.y)
if not self.alive then tint(0) end
if tiny then
tint(255,0,0)
sx,sy = 180,272
else
sx,sy = 66,112
end
self:drawSprite(sx,sy)
popStyle()
popMatrix()
if not tiny then
self.playerSheet:draw()
end
end
function Player:drawSprite(sx,sy)
if self.showDamage then
self:selectDamageTint()
sprite(asset.Character_Princess_Girl_Hit,0,0,sx,sy)
else
sprite(asset.Character_Princess_Girl,0,0,sx,sy)
end
end
And …
function Player:draw(tiny)
local sx,sy
local dx = 0
local dy = 10
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self:graphicCenter() + vec2(dx,dy)
translate(center.x,center.y)
if not self.alive then tint(0) end
sx,sy = self:setForMap(tiny)
self:drawSprite(sx,sy)
popStyle()
popMatrix()
if not tiny then
self.playerSheet:draw()
end
end
function Player:setForMap(tiny)
if tiny then
tint(255,0,0)
return 180,272
else
return 66,112
end
end
Honestly this is still rather nasty, but I’m a bit tired and this is better than I found it. Commit: refactoring player draw.
This works as intended, but the princess can have a bad day:
It’s 1330, let’s sum up.
Summary
We’ve rather readily put in a hit display that highlights a resident when they are hit, and that displays a yellow color if their health is greater than 2, and red otherwise. That particular code is duplicated in Monster and in Player, but the overall draw functions look different. I’ve made a new yellow note: “Combine Player and Monster?”
I even created a new graphic, a very simple one, but new nonetheless. Life is good.
Speaking of life being good, I’ve made another note: “Keep Player alive”. It’s too easy for her to die,. We need to ensure that battles are difficult but possible to win, and rather than restart the game, perhaps we need a way for her to heal, or something that will otherwise protect her from an untimely death.
And there is some fairly tedious work ahead. We need to make multiple levels. We need to populate levels with monsters who are proportionate to the level. And we need treasures.
With treasures, we’ll need an inventory, and perhaps we need to do something to allow the player either to equip certain weapons and armor, or even to apply them in the course of a battle. That last might be challenging.
I’d like to have battles be a bit more engaging. Once the encounter starts, there’s not much to do other than sit there banging the key to try to be the first to start an attack. Maybe there are things we can do during the encounter, or at least to set up for when the encounter ends.
In the movie above, the Princess was in trouble. She ran away, looking for health boxes, but then ran into another monster. Foolishly, she tried to vanquish it. This was not to be. RIP Princess.
See you next time!