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 showDamageflag 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 …

hornet

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

biter

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:

princess

princess_hit

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:

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!


D2.zip