A bit of a tricky issue today. We want our Mimic to have different animation sequences, depending on what’s going on. No other Entity does this at present.

We have a few spritesheets for the Mimic:

Hide

hide

Idle

idle

Walk

walk

Attack

attack

Hurt

hurt

Death

death

(Remind me to be sure that I select the right copies of these. They come in various sizes.)

I imagine a flow something like this:

  1. The Mimic starts out in the hidden condition, displaying a chest just like a regular Chest.
  2. When the player tries to move onto the Mimic, as she would onto a Chest, the Mimic wakes up, and runs the “hide” sequence, which looks to me more like “unhide” from left to right.
  3. If the player moves far enough away while the “hide” is running, the Mimic goes into “idle” and stays where it is, licking its lips or whatever.
  4. Under some condition yet to be defined, perhaps player getting closer, or perhaps just randomly, the Mimic starts walking randomly, using its “walk” animation.
  5. If the player and Mimic get close enough together (probably manhattan distance 2 or 3), the Mimic starts moving toward the player, and starts running the “attack” animation. This will continue so long as they are close enough together, and during any battle that may ensue.
  6. Possibly, when the Mimic is hit, it will run the “hurt” animation. I’m not sure of this.
  7. If the Mimic is defeated, it stops moving and runs the “death” animation. It stays in the final death position thereafter and never wakes up.
  8. If, instead, the player manages to get out of range, the Mimic will return to its idle mode. After a while, or if the player retreats further, it will return to the quiescent looking-like-a-chest mode.
  9. When the Mimic is defeated, it will give up a treasure to be defined.

Now in a “real” game, we would have this kind of thing for all our monsters. They would all have various animations for their various states of mind. In this game, the Mimic is the first Entity to have this kind of behavior.

So our mission–and we do choose to accept it–is to implement this logic for our Mimic-style Monster, in the light of its specialized MimicMonsterStrategy.

Let’s look at a typical entry in the monster table:

    m = {name="Pink Slime", level = 1, health={1,2}, speed = {4,10}, strength=1,
    attackVerbs={"smears", "squishes", "sloshes at"},
    dead=asset.slime_squashed, hit=asset.slime_hit,
    moving={asset.slime, asset.slime_walk, asset.slime_squashed}}

What we see is that when a monster is moving, it has a list of poses to cycle among. But when it is hit, it just has one image, and just one when it is dead. That’s because my original monsters only have a few poses. We can easily extend each of those elements to be tables, typically containing only one animation, and we can add a few other optional ones for hiding and such.

Let’s see how the animation and drawing work.

function Monster:setAnimationTimer()
    if self.animationTimer then tween.stop(self.animationTimer) end
    local times = self.animationTimes
    self.animationTimer = self:setTimer(self.chooseAnimation, times[1], times[2])
end

function Monster:setTimer(action, time, deltaTime)
    if not self.runner then return end
    local t = time + math.random()*deltaTime
    return tween.delay(t, action, self)
end

function Monster:chooseAnimation()
    if self:isAlive() then
        self.movingIndex = self.movingIndex + 1
        if self.movingIndex > #self.moving then self.movingIndex = 1 end
        self:setAnimationTimer()
    else
        self.sprite1 = self.dead
    end
end

Weird code there in chooseAnimation. Was I afraid of the mod operator that day?

Note that when the entity is dead we don’t run its timer any more, leaving it unmoving. But if there was only one element in the animation list, it would just cycle the one. A bit less efficient but could be worth keeping in mind.

We draw like this:

function Monster:drawExplicit()
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end
    self:drawMonster()
    self:drawSheet()
end

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    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(Sprites:sprite(self.dead))
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(Sprites:sprite(self.hit))
    else
        sprite(Sprites:sprite(self.moving[self.movingIndex]))
    end
    popStyle()
    popMatrix()
end

The normal mode is that we show the current image from the self.moving collection. That changes according to the timer. I wonder about the showDamage. Do we even do that any more?

Here’s what sets it:

function Monster:displayDamage(boolean)
    self.showDamage = boolean
end

Are there senders? There are not. That code is dead. I suspected that was the case. We could revive it with a change to CombatRound, and some new commands for the Crawl. That’s for another day, I think.

Clearing a Place to Work

What’s to do? Often, before we do a job, we need to clear a place to work. We move things off our desk, or clear the workbench, or move the car out of the garage. It’s the same with code. Sometimes we need to make a space to do the work before we do the work.

Here’s what I have in mind:

  • Change all Monster-side Entities so that they are always running a list of animation frames, even if they are “dead”.
  • Change Monsters so that all their defined animations are tables, and so that unprovided tables default to a table that is always provided, i.e. moving.
  • Change Monsters so that they animate, not from moving, but from another changeable list, animations.
  • Remove the showHit logic from the system, or at least from draw, to be reimplemented later if we choose.

When this is done, external behavior will be the same as it is now. And by providing more tables to Mimic, and manipulating them in its strategy, we can get the more advanced behavior we want for it.

We prepare the system for more behavior, and when it’s ready, we put in the behavior.

Let’s do it.

First, let’s make movement use an animations list, not moving. We start in the drawMonster

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    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(Sprites:sprite(self.dead))
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(Sprites:sprite(self.hit))
    else
        sprite(Sprites:sprite(self.moving[self.movingIndex]))
    end
    popStyle()
    popMatrix()
end

There’s a lot to do here. First, the change from .moving to .animations.

    sprite(Sprites:sprite(self.moving[self.movingIndex]))

And we need to make sure that’s initialized:

function Monster:init(tile, runner, mtEntry)
    if not MT then self:initMonsterTable() end
    tile:moveObject(self)
    self.runner = runner
    self.mtEntry = mtEntry or self:randomMtEntry()
    self.dead = self.mtEntry.dead
    self.hit = self.mtEntry.hit
    self.moving = self.mtEntry.moving
    self.level = self.mtEntry.level or 1
    self.facing = self.mtEntry.facing or 1
    self.attackVerbs = self.mtEntry.attackVerbs or {"strikes at"}
    self:initAttributes()
    self.movingIndex = 1
    self.animations = self.moving
    self.swap = 0
    self.move = 0
    self.attributeSheet = AttributeSheet(self)
    self.deathSound = asset.downloaded.A_Hero_s_Quest.Monster_Die_1
    self.hurtSound = asset.downloaded.A_Hero_s_Quest.Monster_Hit_1
    self.pitch = 1
    self.animationTimes = self.mtEntry.animationTimes or {0.25,0.25}
    self:startAllTimers()
end

While I’m thinking of it … when we do change a Monster’s animations, we’ll want to set the movingIndex index to 1. In fact, if we don’t, we could wind up with an index outside the range of a shorter list. I’ll extract a method:

function Monster:setAnimation(animations)
    self.movingIndex = 1
    self.animations = animations
end

And use it:

function Monster:init(tile, runner, mtEntry)
    if not MT then self:initMonsterTable() end
    tile:moveObject(self)
    self.runner = runner
    self.mtEntry = mtEntry or self:randomMtEntry()
    self.dead = self.mtEntry.dead
    self.hit = self.mtEntry.hit
    self.moving = self.mtEntry.moving
    self.level = self.mtEntry.level or 1
    self.facing = self.mtEntry.facing or 1
    self.attackVerbs = self.mtEntry.attackVerbs or {"strikes at"}
    self:setAnimation(self.moving) -- < ---
    self:initAttributes()
    self.swap = 0
    self.move = 0
    self.attributeSheet = AttributeSheet(self)
    self.deathSound = asset.downloaded.A_Hero_s_Quest.Monster_Die_1
    self.hurtSound = asset.downloaded.A_Hero_s_Quest.Monster_Hit_1
    self.pitch = 1
    self.animationTimes = self.mtEntry.animationTimes or {0.25,0.25}
    self:startAllTimers()
end

That init is pretty long. We may wish to improve it.

We should have everything working now. Let’s test. All is well. Commit: Monsters animate from animations table, not moving.

Now I want to convert all the current single-frame members to tables. I’m going to do that by moving the table to Sublime Text on my Mac, where I have more powerful editing tools.

OK, I had to relearn regex, of course, but now they all look like this:

    m = {name="Pink Slime", level = 1, health={1,2}, speed = {4,10}, strength=1,
        attackVerbs={"smears", "squishes", "sloshes at"},
        dead={asset.slime_squashed}, hit={asset.slime_hit},
        moving={asset.slime, asset.slime_walk, asset.slime_squashed}}

OK, now all the animation frames are in tables of length 1 or more. I’m not fond of setting the animation inside drawMonster, which will happen many times per second. Instead we should find a place to set it outside. Let’s trim the draw and then fix the bug.

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self:flipTowardPlayer()
    sprite(Sprites:sprite(self.moving[self.movingIndex]))
    popStyle()
    popMatrix()
end

Now, unless I miss my guess. Monsters will continue to move when dead, but will no longer attack.

Ah, once again I miss my guess. They stop moving, because we stop animating here:

function Monster:chooseAnimation()
    if self:isAlive() then
        self.movingIndex = self.movingIndex + 1
        if self.movingIndex > #self.moving then self.movingIndex = 1 end
        self:setAnimationTimer()
    else
        self.sprite1 = self.dead
    end
end

We no longer want to do this. We want to continue animating, but this is a convenient time to pull out the dead animation, at least for now.

I am not at all sure what that sprite is about but we won’t see it happening.

function Monster:chooseAnimation()
    if self:isDead() then
        self:setAnimation(self.dead)
        self.movingIndex = #self.animations -- hack
    end
    self.movingIndex = (self.movingIndex + 1) % #self.animations
    self:setAnimationTimer()
end

I felt that I needed to hack the animation counter movingIndex to make sure we start at the beginning of the cycle. I need to think about that further. For now, I think that now monsters will die, showing their x eyes, will not tint, will stop moving.

Something sploded.

Monster:272: expect userdata, got nil
stack traceback:
	[C]: in function 'sprite'
	Monster:272: in method 'drawMonster'
	Monster:259: in method 'drawExplicit'
	Monsters:76: in method 'draw'
	GameRunner:216: in method 'drawMap'
	GameRunner:202: in method 'drawLargeMap'
	GameRunner:176: in method 'draw'
	Main:34: in function 'draw'

I’ll take a quick look to see if I see the problem, otherwise I’m going to revert and do over. Yes:

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self:flipTowardPlayer()
    sprite(Sprites:sprite(self.moving[self.movingIndex]))
    popStyle()
    popMatrix()
end

I had a weird pasting event that damaged that method in Codea’s editor, and lost the change to animations:

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self:flipTowardPlayer()
    sprite(Sprites:sprite(self.animations[self.movingIndex]))
    popStyle()
    popMatrix()
end

Hm, that didn’t fix it. And I really don’t want to revert. No, let’s revert and paste the edited table back in. I don’t know what’s wrong. Try again, fail better.

OK, we’re back to a safe place. The monsters are using the animations table just fine.

No! The damage has already occurred. I’m not sure what happened but here’s the code for drawMonster:

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    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(Sprites:sprite(self.dead))
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(Sprites:sprite(self.hit))
    else
        function Monster:drawMonster()
            if not self.tile.currentlyVisible then return end
            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(Sprites:sprite(self.dead))
            elseif self.showDamage then
                self:selectDamageTint()
                sprite(Sprites:sprite(self.hit))
            else
                sprite(Sprites:sprite(self.moving[self.movingIndex]))
            end
            popStyle()
            popMatrix()
        end
        
    end
    popStyle()
    popMatrix()
end

That won’t do at all. Delete the weird past in the middle:

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    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(Sprites:sprite(self.dead))
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(Sprites:sprite(self.hit))
    else
        
    end
    popStyle()
    popMatrix()
end

We need something in the else and it should really use self.animations but let’s first use ``self.moving`:

    if self:isDead() then
        tint(0,128,0,175)
        sprite(Sprites:sprite(self.dead))
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(Sprites:sprite(self.hit))
    else
        sprite(Sprites:sprite(self.moving[self.movingIndex]))
    end

Test. OK, that works. Now use animations:

    if self:isDead() then
        tint(0,128,0,175)
        sprite(Sprites:sprite(self.dead))
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(Sprites:sprite(self.hit))
    else
        sprite(Sprites:sprite(self.animations[self.movingIndex]))
    end

Make sure we set them:

function Monster:init(tile, runner, mtEntry)
    if not MT then self:initMonsterTable() end
    tile:moveObject(self)
    self.runner = runner
    self.mtEntry = mtEntry or self:randomMtEntry()
    self.dead = self.mtEntry.dead
    self.hit = self.mtEntry.hit
    self.moving = self.mtEntry.moving
    self.level = self.mtEntry.level or 1
    self.facing = self.mtEntry.facing or 1
    self.attackVerbs = self.mtEntry.attackVerbs or {"strikes at"}
    self:setAnimation(self.moving)
    self:initAttributes()
    self.swap = 0
    self.move = 0
    self.attributeSheet = AttributeSheet(self)
    self.deathSound = asset.downloaded.A_Hero_s_Quest.Monster_Die_1
    self.hurtSound = asset.downloaded.A_Hero_s_Quest.Monster_Hit_1
    self.pitch = 1
    self.animationTimes = self.mtEntry.animationTimes or {0.25,0.25}
    self:startAllTimers()
end

function Monster:setAnimation(animations)
    self.movingIndex = 1
    self.animations = animations
end

Should be OK.

Still good. Commit: fix: main animation uses animations not moving.

Now let’s simplify the draw to always draw from the animations table no matter what.

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self:flipTowardPlayer()
    sprite(Sprites:sprite(self.animations[self.movingIndex]))
    popStyle()
    popMatrix()
end

At this point, I think movingIndex will stop incrementing, because of this:

function Monster:chooseAnimation()
    if self:isAlive() then
        self.movingIndex = self.movingIndex + 1
        if self.movingIndex > #self.moving then self.movingIndex = 1 end
        self:setAnimationTimer()
    else
        self.sprite1 = self.dead
    end
end

What is that sprite1? Any references? No. Remove:

function Monster:chooseAnimation()
    if self:isAlive() then
        self.movingIndex = self.movingIndex + 1
        if self.movingIndex > #self.moving then self.movingIndex = 1 end
        self:setAnimationTimer()
    end
end

Now I expect that a dead monster will stop moving but not show his X-ed out eyes.

A test failed. It’s an intermittent. I know what it is. I’ve been living with it. We’ll look at it shortly but we are in the middle of something now and won’t divert.

Yes, monster just stops animating when dead. Let’s see if we can set the new dead animation.

Yikes, I reverted out my edits from sublime. Put those back: everything the same (should be), commit: all monster animations are tables, even single-frame ones.

Now let’s put back the dead one. I hammered it less hard this time:

function Monster:chooseAnimation()
    if self:isAlive() then
        self.movingIndex = self.movingIndex + 1
        if self.movingIndex > #self.moving then self.movingIndex = 1 end
    else
        self:setAnimation(self.dead)
    end
    self:setAnimationTimer()
end

The monsters are now displaying their dead state properly. I’m tempted to retain the tint feature to make it more obvious that they are down. But no.

What does the mimic have for dead?

    m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy,
        attackVerbs={"bites", "chomps", "gnaws"},
        dead={"mwalk10"}, hit={"mwalk02"},moving={"mattack01", "mattack01", "mattack03", "mattack04", "mattack05", "mattack06", "mattack07", "mattack08", "mattack09", "mattack10"}

Commit: dead monsters animate their dead frames.

Now let’s make sure we have all the Mimic Sheets defined. We have walk, hide, and attack. I’ll add the other three, I’ll have to add them through Dropbox.assets, starting in the Files app. And I’ll rename them to match the others.

Oh hell, I have to covert them to PNG first. Darn.

The process is to import into Procreate (the files are PSD files), save back to the gallery, repeat for each file. Rename the Procreate pictures if you wish. Select them, Share, select PNG, select the Dropbox.assets folder, and voila.

    sheet = asset.mimic_spritesheet_death
    names = { "mdead01", "mdead02", "mdead03", "mdead04","mdead05","mdead06","mdead07","mdead08","mdead09","mdead10"}
    assert(#names==10, "bad count for mimic death")
    Sprites:add(names,sheet, 0,0,0,0)
    sheet = asset.mimic_spritesheet_idle
    names = { "midle01", "midle02", "midle03", "midle04","midle05","midle06","midle07","midle08","midle09","midle10"}
    assert(#names==10, "bad count for mimic idle")
    Sprites:add(names,sheet, 0,0,0,0)
    sheet = asset.mimic_spritesheet_hurt
    names = { "mhurt01", "mhurt02", "mhurt03", "mhurt04","mhurt05","mhurt06","mhurt07","mhurt08","mhurt09","mhurt10"}
    assert(#names==10, "bad count for mimic hurt")
    Sprites:add(names,sheet, 0,0,0,0)

Now let’s give the mimic a better individual dead state, “mdead10”.

    m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy,
        attackVerbs={"bites", "chomps", "gnaws"},
        dead={"mdead10"}, hit={"mwalk02"},moving={"mattack01", "mattack01", "mattack03", "mattack04", "mattack05", "mattack06", "mattack07", "mattack08", "mattack09", "mattack10"}
    }

Let’s go knock one down.

mimic goes down

mimic down

That’s OK for now. He overlaps the frame a bit, but we are allowing that now. Would never happen if we had standard monster sizes. I suppose we could readily resize all the frames for the Mimic. For values of readily.

Commit: Mimic has nice death pose.

I Think We’re Ready

Recall that we’ve been preparing the ground for our more advanced Mimic animation routine. All the monsters’ animation sets are tables now, even if they only have one entry. We animate from a separate table called animations which can be set at any time, using setAnimation, and which will thereafter be used.

Our next step is to give the monsters the ability to have more animation sets, and to default them appropriately.

To do this, I think we’d best extract a method. (So we aren’t as ready as I thought.) Here’s animation init now:

function Monster:init(tile, runner, mtEntry)
    if not MT then self:initMonsterTable() end
    tile:moveObject(self)
    self.runner = runner
    self.mtEntry = mtEntry or self:randomMtEntry()
-- animations
    self.dead = self.mtEntry.dead
    self.hit = self.mtEntry.hit
    self.moving = self.mtEntry.moving
-- animations end
    self.level = self.mtEntry.level or 1
    self.facing = self.mtEntry.facing or 1
    self.attackVerbs = self.mtEntry.attackVerbs or {"strikes at"}
-- animations again
    self:setAnimation(self.moving)
-- animations end again
    self:initAttributes()
    self.swap = 0
    self.move = 0
    self.attributeSheet = AttributeSheet(self)
    self.deathSound = asset.downloaded.A_Hero_s_Quest.Monster_Die_1
    self.hurtSound = asset.downloaded.A_Hero_s_Quest.Monster_Hit_1
    self.pitch = 1
    self.animationTimes = self.mtEntry.animationTimes or {0.25,0.25}
    self:startAllTimers()
end

I’ll extract that stuff:

function Monster:init(tile, runner, mtEntry)
    if not MT then self:initMonsterTable() end
    tile:moveObject(self)
    self.runner = runner
    self.mtEntry = mtEntry or self:randomMtEntry()
    self:initAnimations()
    self.level = self.mtEntry.level or 1
    self.facing = self.mtEntry.facing or 1
    self.attackVerbs = self.mtEntry.attackVerbs or {"strikes at"}
    self:initAttributes()
    self.swap = 0
    self.move = 0
    self.attributeSheet = AttributeSheet(self)
    self.deathSound = asset.downloaded.A_Hero_s_Quest.Monster_Die_1
    self.hurtSound = asset.downloaded.A_Hero_s_Quest.Monster_Hit_1
    self.pitch = 1
    self.animationTimes = self.mtEntry.animationTimes or {0.25,0.25}
    self:startAllTimers()
end

function Monster:initAnimations()
    self.dead = self.mtEntry.dead
    self.hit = self.mtEntry.hit
    self.moving = self.mtEntry.moving
    self:setAnimation(self.moving)
end

Here again, we’re making a safe place to stand while we do our work.

Our mimic knows six animations, hide, idle, walk, attack, hurt, death. We normally expect moving, hit, and dead. We’ll add attack, idle, and hide, to be defaulted like this:

function Monster:initAnimations()
    local mtE = self.mtEntry
    self.dead = mtE.dead
    self.hit = mtE.hit
    self.moving = mtE.moving
    self.attack = mtE.attack or self.moving
    self.idle = mtE.idle or self.moving
    self.hide = mtE.hide or self.moving
    self:setAnimation(self.moving)
end

I believe this leaves us right where we were, even if we did have values provided for our Mimic. Let’s do provide them and then test.

    m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy,
        attackVerbs={"bites", "chomps", "gnaws"},
        dead={"mdead10"},
        hit={"mhurt01", "mhurt02", "mhurt03", "mhurt04", "mhurt05", "mhurt06", "mhurt07", "mhurt08", "mhurt09", "mhurt10"},
        moving={"mwalk01", "mwalk02", "mwalk03", "mwalk04", "mwalk05", "mwalk06", "mwalk07", "mwalk08", "mwalk09", "mwalk10"},
        attack={"mattack01", "mattack02", "mattack03", "mattack04", "mattack05", "mattack06", "mattack07", "mattack08", "mattack09", "mattack10"},
        idle={"midle01", "midle02", "midle03", "midle04", "midle05", "midle06", "midle07", "midle08", "midle09", "midle10"},
        hide={"mhide01", "mhide02", "mhide03", "mhide04", "mhide05", "mhide06", "mhide07", "mhide08", "mhide09", "mhide10"},
    }

I think I got that right. I did notice some typos in the earlier table.

Everything works, with the Mimic now using his walk animation all the time. He will engage in battle if the princess attacks him. I imagine he might do it by accident. What’s his strategy anyway?

Ah, yes:

function MimicMonsterStrategy:selectMove(range)
    if range > 2 then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardPlayer"
    end
end

If we get too close to him, he’ll move toward us and attack. What would happen if we set his animation right there?

function MimicMonsterStrategy:selectMove(range)
    if range > 2 then
        self.monster:setAnimation(self.monster.moving)
        return "basicMoveRandomly"
    else
        self.monster:setAnimation(self.monster.attack)
        return "basicMoveTowardPlayer"
    end
end

attack and back

So that worked as intended. When we get too close, he starts running his attack animation, and when I managed to get away, by crossing a tile he couldn’t cross, he went back to his more calm walking sequence.

It’s about lunch time here at the homestead, so let’s commit this baby and carry on next time. Commit: mimic monster changes animation depending on strategy state.

Summing Up

Making a place to stand served us well today. When the objects aren’t quite right for a new capability, we can proceed in at least these ways:

  1. Code the new capability, adjusting the objects as we go.
  2. Adjust the objects first, then code the new capability in accord with the new scheme.
  3. Code the new capability separately and then figure out a merge.
  4. Code the new capability separately and then don’t merge.

Ways 3 and 4 tend to be cut and paste fests. Way 3 tries to clean things up, way for is basically giving up, and leaves us with duplicate code all over.

Way 1 is tricky, because we are trying to write new capability as we go, but since we’re changing how existing things work, we tend to break them. Our attention is split between adjusting the old and building the new.

And way 2 is the one we picked today. We make the code consistent with whatever’s needed for our new feature, then we implement that feature in the nice existing ready-for-us code.

This is generally my favorite way to go, and from the way I’ve described them, you can probably see why. Of course nothing is absolute, and I don’t always do this. Perhaps I should, but sometimes I don’t really see what I want until I do it.

In that situation one might be best served by doing a prototype and then reverting and starting over, but it takes some discipline to do that.

I did make some mistake along the way and cleverly reverted after a quick attempt to debug it. I don’t even know what the mistake was. I moved slightly differently the second time, and it went well.

And then I did a second little place to stand, when I extracted the code that sets up a monster’s animations. That could have been done in line but since those lines are clearly not the same as the others, it makes sense to extract them anyway, and once extracted they are much easier to deal with.

All in all, good steps along the way. I think we can proceed to elaborate the Mimic’s strategy, and that will handle a big chunk of his more advanced animation behavior. But not all of it: we want to run some of his sequences just once, and we don’t have a way to do that … yet.

But I bet we’ll have one soon. See you next time!


D2.zip